mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 14:27:22 +01:00
Merge branch 'MDL-46191-master' of git://github.com/FMCorz/moodle
This commit is contained in:
commit
cf2d4159b7
47
grade/report/history/classes/event/grade_report_viewed.php
Normal file
47
grade/report/history/classes/event/grade_report_viewed.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Grade history report viewed event.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2014 Mark Nelson <markn@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace gradereport_history\event;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Grade history report viewed event class.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @since Moodle 2.8
|
||||
* @copyright 2014 Mark Nelson <markn@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class grade_report_viewed extends \core\event\grade_report_viewed {
|
||||
|
||||
/**
|
||||
* Returns localised general event name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_name() {
|
||||
return get_string('eventgradereportviewed', 'gradereport_history');
|
||||
}
|
||||
}
|
94
grade/report/history/classes/filter_form.php
Normal file
94
grade/report/history/classes/filter_form.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Form for grade history filters
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace gradereport_history;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
require_once($CFG->libdir.'/formslib.php');
|
||||
|
||||
/**
|
||||
* Form for grade history filters
|
||||
*
|
||||
* @since Moodle 2.8
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class filter_form extends \moodleform {
|
||||
|
||||
/**
|
||||
* Definition of the Mform for filters displayed in the report.
|
||||
*/
|
||||
public function definition() {
|
||||
|
||||
$mform = $this->_form;
|
||||
$course = $this->_customdata['course'];
|
||||
$itemids = $this->_customdata['itemids'];
|
||||
$graders = $this->_customdata['graders'];
|
||||
$userbutton = $this->_customdata['userbutton'];
|
||||
$names = \html_writer::span('', 'selectednames');
|
||||
|
||||
$mform->addElement('static', 'userselect', get_string('selectusers', 'gradereport_history'), $userbutton);
|
||||
$mform->addElement('static', 'selectednames', get_string('selectedusers', 'gradereport_history'), $names);
|
||||
|
||||
$mform->addElement('select', 'itemid', get_string('gradeitem', 'grades'), $itemids);
|
||||
$mform->setType('itemid', PARAM_INT);
|
||||
|
||||
$mform->addElement('select', 'grader', get_string('grader', 'gradereport_history'), $graders);
|
||||
$mform->setType('grader', PARAM_INT);
|
||||
|
||||
$mform->addElement('date_selector', 'datefrom', get_string('datefrom', 'gradereport_history'), array('optional' => true));
|
||||
$mform->addElement('date_selector', 'datetill', get_string('datetill', 'gradereport_history'), array('optional' => true));
|
||||
|
||||
$mform->addElement('checkbox', 'revisedonly', get_string('revisedonly', 'gradereport_history'));
|
||||
$mform->addHelpButton('revisedonly', 'revisedonly', 'gradereport_history');
|
||||
|
||||
$mform->addElement('hidden', 'id', $course->id);
|
||||
$mform->setType('id', PARAM_INT);
|
||||
|
||||
$mform->addElement('hidden', 'userids');
|
||||
$mform->setType('userids', PARAM_SEQUENCE);
|
||||
|
||||
$mform->addElement('hidden', 'userfullnames');
|
||||
$mform->setType('userfullnames', PARAM_TEXT);
|
||||
|
||||
// Add a submit button.
|
||||
$mform->addElement('submit', 'submitbutton', get_string('submit'));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method implements changes to the form that need to be made once the form data is set.
|
||||
*/
|
||||
public function definition_after_data() {
|
||||
$mform = $this->_form;
|
||||
|
||||
if ($userfullnames = $mform->getElementValue('userfullnames')) {
|
||||
$mform->getElement('selectednames')->setValue(\html_writer::span($userfullnames, 'selectednames'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
187
grade/report/history/classes/helper.php
Normal file
187
grade/report/history/classes/helper.php
Normal file
@ -0,0 +1,187 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Helper class for gradehistory report.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace gradereport_history;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
/**
|
||||
* Helper class for gradehistory report.
|
||||
*
|
||||
* @since Moodle 2.8
|
||||
* @package gradereport_history
|
||||
* @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class helper {
|
||||
|
||||
/**
|
||||
* Initialise the js to handle the user selection {@link gradereport_history_user_button}.
|
||||
*
|
||||
* @param int $courseid course id.
|
||||
* @param array $currentusers List of currently selected users.
|
||||
*
|
||||
* @return output\user_button the user select button.
|
||||
*/
|
||||
public static function init_js($courseid, array $currentusers = null) {
|
||||
global $PAGE;
|
||||
|
||||
// Load the strings for js.
|
||||
$PAGE->requires->strings_for_js(array(
|
||||
'errajaxsearch',
|
||||
'finishselectingusers',
|
||||
'foundoneuser',
|
||||
'foundnusers',
|
||||
'loadmoreusers',
|
||||
'selectusers',
|
||||
), 'gradereport_history');
|
||||
$PAGE->requires->strings_for_js(array(
|
||||
'loading'
|
||||
), 'admin');
|
||||
$PAGE->requires->strings_for_js(array(
|
||||
'noresults',
|
||||
'search'
|
||||
), 'moodle');
|
||||
|
||||
$arguments = array(
|
||||
'courseid' => $courseid,
|
||||
'ajaxurl' => '/grade/report/history/users_ajax.php',
|
||||
'url' => $PAGE->url->out(false),
|
||||
'selectedUsers' => $currentusers,
|
||||
);
|
||||
|
||||
// Load the yui module.
|
||||
$PAGE->requires->yui_module(
|
||||
'moodle-gradereport_history-userselector',
|
||||
'Y.M.gradereport_history.UserSelector.init',
|
||||
array($arguments)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of users.
|
||||
*
|
||||
* We're interested in anyone that had a grade history in this course. This api returns a list of such users based on various
|
||||
* criteria passed.
|
||||
*
|
||||
* @param \context $context Context of the page where the results would be shown.
|
||||
* @param string $search the text to search for (empty string = find all).
|
||||
* @param int $page page number, defaults to 0.
|
||||
* @param int $perpage Number of entries to display per page, defaults to 0.
|
||||
*
|
||||
* @return array list of users.
|
||||
*/
|
||||
public static function get_users($context, $search = '', $page = 0, $perpage = 25) {
|
||||
global $DB;
|
||||
|
||||
list($sql, $params) = self::get_users_sql_and_params($context, $search);
|
||||
$limitfrom = $page * $perpage;
|
||||
$limitto = $limitfrom + $perpage;
|
||||
$users = $DB->get_records_sql($sql, $params, $limitfrom, $limitto);
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of users present for the given search criteria.
|
||||
*
|
||||
* @param \context $context Context of the page where the results would be shown.
|
||||
* @param string $search the text to search for (empty string = find all).
|
||||
*
|
||||
* @return int number of users found.
|
||||
*/
|
||||
public static function get_users_count($context, $search = '') {
|
||||
global $DB;
|
||||
|
||||
list($sql, $params) = self::get_users_sql_and_params($context, $search, true);
|
||||
return $DB->count_records_sql($sql, $params);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sql and params to use to get list of users.
|
||||
*
|
||||
* @param \context $context Context of the page where the results would be shown.
|
||||
* @param string $search the text to search for (empty string = find all).
|
||||
* @param bool $count setting this to true, returns an sql to get count only instead of the complete data records.
|
||||
*
|
||||
* @return array sql and params list
|
||||
*/
|
||||
protected static function get_users_sql_and_params($context, $search = '', $count = false) {
|
||||
|
||||
// Fields we need from the user table.
|
||||
$extrafields = get_extra_user_fields($context);
|
||||
$params = array();
|
||||
if (!empty($search)) {
|
||||
list($filtersql, $params) = users_search_sql($search, 'u', true, $extrafields);
|
||||
$filtersql .= ' AND ';
|
||||
} else {
|
||||
$filtersql = '';
|
||||
}
|
||||
|
||||
$ufields = \user_picture::fields('u', $extrafields).',u.username';
|
||||
if ($count) {
|
||||
$select = "SELECT COUNT(DISTINCT u.id) ";
|
||||
$orderby = "";
|
||||
} else {
|
||||
$select = "SELECT DISTINCT $ufields ";
|
||||
$orderby = " ORDER BY u.lastname ASC, u.firstname ASC";
|
||||
}
|
||||
$sql = "$select
|
||||
FROM {user} u
|
||||
JOIN {grade_grades_history} ggh ON u.id = ggh.userid
|
||||
JOIN {grade_items} gi ON gi.id = ggh.itemid
|
||||
WHERE $filtersql gi.courseid = :courseid";
|
||||
$sql .= $orderby;
|
||||
$params['courseid'] = $context->instanceid;
|
||||
|
||||
return array($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of graders.
|
||||
*
|
||||
* @param int $courseid Id of course for which we need to fetch graders.
|
||||
*
|
||||
* @return array list of graders.
|
||||
*/
|
||||
public static function get_graders($courseid) {
|
||||
global $DB;
|
||||
|
||||
$ufields = get_all_user_name_fields(true, 'u');
|
||||
$sql = "SELECT u.id, $ufields
|
||||
FROM {user} u
|
||||
JOIN {grade_grades_history} ggh ON ggh.usermodified = u.id
|
||||
JOIN {grade_items} gi ON gi.id = ggh.itemid
|
||||
WHERE gi.courseid = :courseid
|
||||
GROUP BY u.id, $ufields
|
||||
ORDER BY u.lastname ASC, u.firstname ASC";
|
||||
|
||||
$graders = $DB->get_records_sql($sql, array('courseid' => $courseid));
|
||||
$return = array(0 => get_string('allgraders', 'gradereport_history'));
|
||||
foreach ($graders as $grader) {
|
||||
$return[$grader->id] = fullname($grader);
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
}
|
112
grade/report/history/classes/output/renderer.php
Normal file
112
grade/report/history/classes/output/renderer.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Renderer for history grade report.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace gradereport_history\output;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
/**
|
||||
* Renderer for history grade report.
|
||||
*
|
||||
* @since Moodle 2.8
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class renderer extends \plugin_renderer_base {
|
||||
|
||||
/**
|
||||
* Render for the select user button.
|
||||
*
|
||||
* @param user_button $button instance of gradereport_history_user_button to render
|
||||
*
|
||||
* @return string HTML to display
|
||||
*/
|
||||
protected function render_user_button(user_button $button) {
|
||||
$attributes = array('type' => 'button',
|
||||
'class' => 'selectortrigger',
|
||||
'value' => $button->label,
|
||||
'disabled' => $button->disabled ? 'disabled' : null,
|
||||
'title' => $button->tooltip);
|
||||
|
||||
if ($button->actions) {
|
||||
$id = \html_writer::random_id('single_button');
|
||||
$attributes['id'] = $id;
|
||||
foreach ($button->actions as $action) {
|
||||
$this->add_action_handler($action, $id);
|
||||
}
|
||||
}
|
||||
// First the input element.
|
||||
$output = \html_writer::empty_tag('input', $attributes);
|
||||
|
||||
// Then hidden fields.
|
||||
$params = $button->url->params();
|
||||
if ($button->method === 'post') {
|
||||
$params['sesskey'] = sesskey();
|
||||
}
|
||||
foreach ($params as $var => $val) {
|
||||
$output .= \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => $var, 'value' => $val));
|
||||
}
|
||||
|
||||
// Then div wrapper for xhtml strictness.
|
||||
$output = \html_writer::tag('div', $output);
|
||||
|
||||
// Now the form itself around it.
|
||||
if ($button->method === 'get') {
|
||||
$url = $button->url->out_omit_querystring(true); // Url without params, the anchor part allowed.
|
||||
} else {
|
||||
$url = $button->url->out_omit_querystring(); // Url without params, the anchor part not allowed.
|
||||
}
|
||||
if ($url === '') {
|
||||
$url = '#'; // There has to be always some action.
|
||||
}
|
||||
$attributes = array('method' => $button->method,
|
||||
'action' => $url,
|
||||
'id' => $button->formid);
|
||||
$output = \html_writer::tag('div', $output, $attributes);
|
||||
|
||||
// Finally one more wrapper with class.
|
||||
return \html_writer::tag('div', $output, array('class' => $button->class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the html for the table.
|
||||
*
|
||||
* @param tablelog $tablelog table object.
|
||||
*
|
||||
* @return string table html
|
||||
*/
|
||||
protected function render_tablelog(tablelog $tablelog) {
|
||||
$o = '';
|
||||
ob_start();
|
||||
$tablelog->out($tablelog->pagesize, false);
|
||||
$o = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
return $o;
|
||||
}
|
||||
|
||||
}
|
433
grade/report/history/classes/output/tablelog.php
Normal file
433
grade/report/history/classes/output/tablelog.php
Normal file
@ -0,0 +1,433 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Renderable class for gradehistory report.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace gradereport_history\output;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
require_once($CFG->libdir . '/tablelib.php');
|
||||
|
||||
/**
|
||||
* Renderable class for gradehistory report.
|
||||
*
|
||||
* @since Moodle 2.8
|
||||
* @package gradereport_history
|
||||
* @copyright 2014 onwards Ankit Agarwal <ankit.agrr@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tablelog extends \table_sql implements \renderable {
|
||||
|
||||
/**
|
||||
* @var int course id.
|
||||
*/
|
||||
protected $courseid;
|
||||
|
||||
/**
|
||||
* @var \context context of the page to be rendered.
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @var \stdClass A list of filters to be applied to the sql query.
|
||||
*/
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* @var array A list of grade items present in the course.
|
||||
*/
|
||||
protected $gradeitems = array();
|
||||
|
||||
/**
|
||||
* @var \course_modinfo|null A list of cm instances in course.
|
||||
*/
|
||||
protected $cms;
|
||||
|
||||
/**
|
||||
* Sets up the table_log parameters.
|
||||
*
|
||||
* @param string $uniqueid unique id of table.
|
||||
* @param \context_course $context Context of the report.
|
||||
* @param \moodle_url $url url of the page where this table would be displayed.
|
||||
* @param array $filters options are:
|
||||
* userids : limit to specific users (default: none)
|
||||
* itemid : limit to specific grade item (default: all)
|
||||
* grader : limit to specific graders (default: all)
|
||||
* datefrom : start of date range
|
||||
* datetill : end of date range
|
||||
* revisedonly : only show revised grades (default: false)
|
||||
* format : page | csv | excel (default: page)
|
||||
* @param string $download Represents download format, pass '' no download at this time.
|
||||
* @param int $page The current page being displayed.
|
||||
* @param int $perpage Number of rules to display per page.
|
||||
*/
|
||||
public function __construct($uniqueid, \context_course $context, $url, $filters = array(), $download = '', $page = 0,
|
||||
$perpage = 100) {
|
||||
parent::__construct($uniqueid);
|
||||
|
||||
$this->set_attribute('class', 'gradereport_history generaltable generalbox');
|
||||
|
||||
// Set protected properties.
|
||||
$this->context = $context;
|
||||
$this->courseid = $this->context->instanceid;
|
||||
$this->pagesize = $perpage;
|
||||
$this->page = $page;
|
||||
$this->filters = (object)$filters;
|
||||
$this->gradeitems = \grade_item::fetch_all(array('courseid' => $this->courseid));
|
||||
$this->cms = get_fast_modinfo($this->courseid);
|
||||
$this->useridfield = 'userid';
|
||||
|
||||
// Define columns in the table.
|
||||
$this->define_table_columns();
|
||||
|
||||
// Define configs.
|
||||
$this->define_table_configs($url);
|
||||
|
||||
// Set download satus.
|
||||
$this->is_downloading($download);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define table configs.
|
||||
*
|
||||
* @param \moodle_url $url url of the page where this table would be displayed.
|
||||
*/
|
||||
protected function define_table_configs(\moodle_url $url) {
|
||||
|
||||
// Set table url.
|
||||
$urlparams = (array)$this->filters;
|
||||
unset($urlparams['submitbutton']);
|
||||
unset($urlparams['userfullnames']);
|
||||
$url->params($urlparams);
|
||||
$this->define_baseurl($url);
|
||||
|
||||
// Set table configs.
|
||||
$this->collapsible(true);
|
||||
$this->sortable(true, 'timemodified', SORT_DESC);
|
||||
$this->pageable(true);
|
||||
$this->no_sorting('grader');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the headers for the html table.
|
||||
*/
|
||||
protected function define_table_columns() {
|
||||
$extrafields = get_extra_user_fields($this->context);
|
||||
|
||||
// Define headers and columns.
|
||||
$cols = array(
|
||||
'timemodified' => get_string('datetime', 'gradereport_history'),
|
||||
'fullname' => get_string('name')
|
||||
);
|
||||
|
||||
// Add headers for extra user fields.
|
||||
foreach ($extrafields as $field) {
|
||||
if (get_string_manager()->string_exists($field, 'moodle')) {
|
||||
$cols[$field] = get_string($field);
|
||||
} else {
|
||||
$cols[$field] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining headers.
|
||||
$cols = array_merge($cols, array(
|
||||
'itemname' => get_string('gradeitem', 'grades'),
|
||||
'prevgrade' => get_string('gradeold', 'gradereport_history'),
|
||||
'finalgrade' => get_string('gradenew', 'gradereport_history'),
|
||||
'grader' => get_string('grader', 'gradereport_history'),
|
||||
'source' => get_string('source', 'gradereport_history'),
|
||||
'overridden' => get_string('overridden', 'grades'),
|
||||
'locked' => get_string('locked', 'grades'),
|
||||
'excluded' => get_string('excluded', 'gradereport_history'),
|
||||
'feedback' => get_string('feedbacktext', 'gradereport_history')
|
||||
)
|
||||
);
|
||||
|
||||
$this->define_columns(array_keys($cols));
|
||||
$this->define_headers(array_values($cols));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to display column timemodifed.
|
||||
*
|
||||
* @param \stdClass $history an entry of history record.
|
||||
*
|
||||
* @return string HTML to display
|
||||
*/
|
||||
public function col_timemodified(\stdClass $history) {
|
||||
return userdate($history->timemodified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to display column itemname.
|
||||
*
|
||||
* @param \stdClass $history an entry of history record.
|
||||
*
|
||||
* @return string HTML to display
|
||||
*/
|
||||
public function col_itemname(\stdClass $history) {
|
||||
// Make sure grade item is still present and link it to the module if possible.
|
||||
$itemid = $history->itemid;
|
||||
if (!empty($this->gradeitems[$itemid])) {
|
||||
if ($history->itemtype === 'mod' && !$this->is_downloading()) {
|
||||
if (!empty($this->cms->instances[$history->itemmodule][$history->iteminstance])) {
|
||||
$cm = $this->cms->instances[$history->itemmodule][$history->iteminstance];
|
||||
$url = new \moodle_url('/mod/' . $history->itemmodule . '/view.php', array('id' => $cm->id));
|
||||
return \html_writer::link($url, $this->gradeitems[$itemid]->get_name());
|
||||
}
|
||||
}
|
||||
return $this->gradeitems[$itemid]->get_name();
|
||||
}
|
||||
return get_string('deleteditemid', 'gradereport_history', $history->itemid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to display column grader.
|
||||
*
|
||||
* @param \stdClass $history an entry of history record.
|
||||
*
|
||||
* @return string HTML to display
|
||||
*/
|
||||
public function col_grader(\stdClass $history) {
|
||||
$grader = new \stdClass();
|
||||
$grader = username_load_fields_from_object($grader, $history, 'grader');
|
||||
$name = fullname($grader);
|
||||
|
||||
if ($this->download) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$userid = $history->usermodified;
|
||||
$profileurl = new \moodle_url('/user/view.php', array('id' => $userid, 'course' => $this->courseid));
|
||||
|
||||
return \html_writer::link($profileurl, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to display column overridden.
|
||||
*
|
||||
* @param \stdClass $history an entry of history record.
|
||||
*
|
||||
* @return string HTML to display
|
||||
*/
|
||||
public function col_overridden(\stdClass $history) {
|
||||
return $history->overridden ? get_string('yes') : get_string('no');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to display column locked.
|
||||
*
|
||||
* @param \stdClass $history an entry of history record.
|
||||
*
|
||||
* @return string HTML to display
|
||||
*/
|
||||
public function col_locked(\stdClass $history) {
|
||||
return $history->locked ? get_string('yes') : get_string('no');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to display column excluded.
|
||||
*
|
||||
* @param \stdClass $history an entry of history record.
|
||||
*
|
||||
* @return string HTML to display
|
||||
*/
|
||||
public function col_excluded(\stdClass $history) {
|
||||
return $history->excluded ? get_string('yes') : get_string('no');
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to display column feedback.
|
||||
*
|
||||
* @param \stdClass $history an entry of history record.
|
||||
*
|
||||
* @return string HTML to display
|
||||
*/
|
||||
public function col_feedback(\stdClass $history) {
|
||||
if ($this->is_downloading()) {
|
||||
return $history->feedback;
|
||||
} else {
|
||||
return format_text($history->feedback, $history->feedbackformat, array('context' => $this->context));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the sql and param list needed, based on the user selected filters.
|
||||
*
|
||||
* @return array containing sql to use and an array of params.
|
||||
*/
|
||||
protected function get_filters_sql_and_params() {
|
||||
global $DB;
|
||||
|
||||
$coursecontext = $this->context;
|
||||
$filter = 'gi.courseid = :courseid';
|
||||
$params = array(
|
||||
'courseid' => $coursecontext->instanceid,
|
||||
);
|
||||
|
||||
if (!empty($this->filters->itemid)) {
|
||||
$filter .= ' AND ggh.itemid = :itemid';
|
||||
$params['itemid'] = $this->filters->itemid;
|
||||
}
|
||||
if (!empty($this->filters->userids)) {
|
||||
$list = explode(',', $this->filters->userids);
|
||||
list($insql, $plist) = $DB->get_in_or_equal($list, SQL_PARAMS_NAMED);
|
||||
$filter .= " AND ggh.userid $insql";
|
||||
$params += $plist;
|
||||
}
|
||||
if (!empty($this->filters->datefrom)) {
|
||||
$filter .= " AND ggh.timemodified >= :datefrom";
|
||||
$params += array('datefrom' => $this->filters->datefrom);
|
||||
}
|
||||
if (!empty($this->filters->datetill)) {
|
||||
$filter .= " AND ggh.timemodified <= :datetill";
|
||||
$params += array('datetill' => $this->filters->datetill);
|
||||
}
|
||||
if (!empty($this->filters->grader)) {
|
||||
$filter .= " AND ggh.usermodified = :grader";
|
||||
$params += array('grader' => $this->filters->grader);
|
||||
}
|
||||
|
||||
return array($filter, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the complete sql with all the joins to get the grade history data.
|
||||
*
|
||||
* @param bool $count setting this to true, returns an sql to get count only instead of the complete data records.
|
||||
*
|
||||
* @return array containing sql to use and an array of params.
|
||||
*/
|
||||
protected function get_sql_and_params($count = false) {
|
||||
$fields = 'ggh.id, ggh.timemodified, ggh.itemid, ggh.userid, ggh.finalgrade, ggh.usermodified,
|
||||
ggh.source, ggh.overridden, ggh.locked, ggh.excluded, ggh.feedback, ggh.feedbackformat,
|
||||
gi.itemtype, gi.itemmodule, gi.iteminstance, gi.itemnumber, ';
|
||||
|
||||
// Add extra user fields that we need for the graded user.
|
||||
$extrafields = get_extra_user_fields($this->context);
|
||||
foreach ($extrafields as $field) {
|
||||
$fields .= 'u.' . $field . ', ';
|
||||
}
|
||||
$gradeduserfields = get_all_user_name_fields(true, 'u');
|
||||
$fields .= $gradeduserfields . ', ';
|
||||
$groupby = $fields;
|
||||
|
||||
// Add extra user fields that we need for the grader user.
|
||||
$fields .= get_all_user_name_fields(true, 'ug', '', 'grader');
|
||||
$groupby .= get_all_user_name_fields(true, 'ug');
|
||||
|
||||
// Filtering on revised grades only.
|
||||
$revisedonly = !empty($this->filters->revisedonly);
|
||||
|
||||
if ($count && !$revisedonly) {
|
||||
// We can only directly use count when not using the filter revised only.
|
||||
$select = "COUNT(1)";
|
||||
} else {
|
||||
// Fetching the previous grade. We use MAX() to ensure that we only get one result if
|
||||
// more than one histories happened at the same second.
|
||||
$prevgrade = "SELECT MAX(finalgrade)
|
||||
FROM {grade_grades_history} h
|
||||
WHERE h.itemid = ggh.itemid
|
||||
AND h.userid = ggh.userid
|
||||
AND h.timemodified < ggh.timemodified
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM {grade_grades_history} h2
|
||||
WHERE h2.itemid = ggh.itemid
|
||||
AND h2.userid = ggh.userid
|
||||
AND h2.timemodified < ggh.timemodified
|
||||
AND h.timemodified < h2.timemodified)";
|
||||
|
||||
$select = "$fields, ($prevgrade) AS prevgrade,
|
||||
CASE WHEN gi.itemname IS NULL THEN gi.itemtype ELSE gi.itemname END AS itemname";
|
||||
}
|
||||
|
||||
list($where, $params) = $this->get_filters_sql_and_params();
|
||||
|
||||
$sql = "SELECT $select
|
||||
FROM {grade_grades_history} ggh
|
||||
LEFT JOIN {grade_items} gi ON gi.id = ggh.itemid
|
||||
JOIN {user} u ON u.id = ggh.userid
|
||||
JOIN {user} ug ON ug.id = ggh.usermodified
|
||||
WHERE $where";
|
||||
|
||||
// As prevgrade is a dynamic field, we need to wrap the query. This is the only filtering
|
||||
// that should be defined outside the method self::get_filters_sql_and_params().
|
||||
if ($revisedonly) {
|
||||
$allorcount = $count ? 'COUNT(1)' : '*';
|
||||
$sql = "SELECT $allorcount FROM ($sql) pg
|
||||
WHERE pg.finalgrade != pg.prevgrade
|
||||
OR (pg.prevgrade IS NULL AND pg.finalgrade IS NOT NULL)
|
||||
OR (pg.prevgrade IS NOT NULL AND pg.finalgrade IS NULL)";
|
||||
}
|
||||
|
||||
// Add order by if needed.
|
||||
if (!$count && $this->get_sql_sort()) {
|
||||
$sql .= " ORDER BY " . $this->get_sql_sort();
|
||||
}
|
||||
|
||||
return array($sql, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the reader. Store results in the object for use by build_table.
|
||||
*
|
||||
* @param int $pagesize size of page for paginated displayed table.
|
||||
* @param bool $useinitialsbar do you want to use the initials bar.
|
||||
*/
|
||||
public function query_db($pagesize, $useinitialsbar = true) {
|
||||
global $DB;
|
||||
|
||||
list($countsql, $countparams) = $this->get_sql_and_params(true);
|
||||
list($sql, $params) = $this->get_sql_and_params();
|
||||
$total = $DB->count_records_sql($countsql, $countparams);
|
||||
$this->pagesize($pagesize, $total);
|
||||
$histories = $DB->get_records_sql($sql, $params, $this->pagesize * $this->page, $this->pagesize);
|
||||
foreach ($histories as $history) {
|
||||
$this->rawdata[] = $history;
|
||||
}
|
||||
// Set initial bars.
|
||||
if ($useinitialsbar) {
|
||||
$this->initialbars($total > $pagesize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of selected users.
|
||||
*
|
||||
* @return array returns an array in the format $userid => $userid
|
||||
*/
|
||||
public function get_selected_users() {
|
||||
global $DB;
|
||||
$idlist = array();
|
||||
if (!empty($this->filters->userids)) {
|
||||
|
||||
$idlist = explode(',', $this->filters->userids);
|
||||
list($where, $params) = $DB->get_in_or_equal($idlist);
|
||||
return $DB->get_records_select('user', "id $where", $params);
|
||||
|
||||
}
|
||||
return $idlist;
|
||||
}
|
||||
|
||||
}
|
52
grade/report/history/classes/output/user_button.php
Normal file
52
grade/report/history/classes/output/user_button.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* User button. Adapted from core_select_user_button.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace gradereport_history\output;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
/**
|
||||
* A button that is used to select users for a form.
|
||||
*
|
||||
* @since Moodle 2.8
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class user_button extends \single_button implements \renderable {
|
||||
/**
|
||||
* Initialises the new select_user_button.
|
||||
*
|
||||
* @param \moodle_url $url
|
||||
* @param string $label The text to display in the button
|
||||
* @param string $method Either post or get
|
||||
*/
|
||||
public function __construct(\moodle_url $url, $label, $method = 'post') {
|
||||
parent::__construct($url, $label, $method);
|
||||
$this->class = 'singlebutton selectusersbutton gradereport_history_plugin';
|
||||
$this->formid = \html_writer::random_id('selectusersbutton');
|
||||
}
|
||||
}
|
41
grade/report/history/db/access.php
Normal file
41
grade/report/history/db/access.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Capability definition for the gradebook grader report
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$capabilities = array(
|
||||
|
||||
'gradereport/history:view' => array(
|
||||
'riskbitmask' => RISK_PERSONAL,
|
||||
'captype' => 'read',
|
||||
'contextlevel' => CONTEXT_COURSE,
|
||||
'archetypes' => array(
|
||||
'teacher' => CAP_ALLOW,
|
||||
'editingteacher' => CAP_ALLOW,
|
||||
'manager' => CAP_ALLOW
|
||||
),
|
||||
'clonepermissionsfrom' => 'gradereport/grader:view'
|
||||
)
|
||||
);
|
118
grade/report/history/index.php
Normal file
118
grade/report/history/index.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The gradebook grade history report
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
require_once($CFG->libdir.'/gradelib.php');
|
||||
require_once($CFG->dirroot.'/grade/lib.php');
|
||||
|
||||
$download = optional_param('download', '', PARAM_ALPHA);
|
||||
$courseid = required_param('id', PARAM_INT); // Course id.
|
||||
$page = optional_param('page', 0, PARAM_INT); // Active page.
|
||||
|
||||
$PAGE->set_pagelayout('report');
|
||||
$url = new moodle_url('/grade/report/history/index.php', array('id' => $courseid));
|
||||
$PAGE->set_url($url);
|
||||
|
||||
$course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
|
||||
require_login($course);
|
||||
$context = context_course::instance($course->id);
|
||||
|
||||
require_capability('gradereport/history:view', $context);
|
||||
require_capability('moodle/grade:viewall', $context);
|
||||
|
||||
// Last selected report session tracking.
|
||||
if (!isset($USER->grade_last_report)) {
|
||||
$USER->grade_last_report = array();
|
||||
}
|
||||
$USER->grade_last_report[$course->id] = 'history';
|
||||
|
||||
$select = "itemtype != 'course' AND itemname != '' AND courseid = :courseid";
|
||||
$itemids = $DB->get_records_select_menu('grade_items', $select, array('courseid' => $course->id), 'itemname ASC', 'id, itemname');
|
||||
$itemids = array(0 => get_string('allgradeitems', 'gradereport_history')) + $itemids;
|
||||
|
||||
$output = $PAGE->get_renderer('gradereport_history');
|
||||
$graders = \gradereport_history\helper::get_graders($course->id);
|
||||
$params = array('course' => $course, 'itemids' => $itemids, 'graders' => $graders, 'userbutton' => null);
|
||||
$mform = new \gradereport_history\filter_form(null, $params);
|
||||
$filters = array();
|
||||
if ($data = $mform->get_data()) {
|
||||
$filters = (array)$data;
|
||||
|
||||
if (!empty($filters['datetill'])) {
|
||||
$filters['datetill'] += DAYSECS - 1; // Set to end of the chosen day.
|
||||
}
|
||||
} else {
|
||||
$filters = array(
|
||||
'id' => $courseid,
|
||||
'userids' => optional_param('userids', '', PARAM_SEQUENCE),
|
||||
'itemid' => optional_param('itemid', 0, PARAM_INT),
|
||||
'grader' => optional_param('grader', 0, PARAM_INT),
|
||||
'datefrom' => optional_param('datefrom', 0, PARAM_INT),
|
||||
'datetill' => optional_param('datetill', 0, PARAM_INT),
|
||||
'revisedonly' => optional_param('revisedonly', 0, PARAM_INT),
|
||||
);
|
||||
}
|
||||
|
||||
$table = new \gradereport_history\output\tablelog('gradereport_history', $context, $url, $filters, $download, $page);
|
||||
|
||||
$names = array();
|
||||
foreach ($table->get_selected_users() as $key => $user) {
|
||||
$names[$key] = fullname($user);
|
||||
}
|
||||
$filters['userfullnames'] = implode(',', $names);
|
||||
|
||||
// Set up js.
|
||||
\gradereport_history\helper::init_js($course->id, $names);
|
||||
|
||||
// Now that we have the names, reinitialise the button so its able to control them.
|
||||
$button = new \gradereport_history\output\user_button($PAGE->url, get_string('selectusers', 'gradereport_history'), 'get');
|
||||
|
||||
$userbutton = $output->render($button);
|
||||
$params = array('course' => $course, 'itemids' => $itemids, 'graders' => $graders, 'userbutton' => $userbutton);
|
||||
$mform = new \gradereport_history\filter_form(null, $params);
|
||||
$mform->set_data($filters);
|
||||
|
||||
if ($table->is_downloading()) {
|
||||
// Download file and exit.
|
||||
echo $output->render($table);
|
||||
die();
|
||||
}
|
||||
|
||||
// Print header.
|
||||
print_grade_page_head($COURSE->id, 'report', 'history', get_string('pluginname', 'gradereport_history'), false, '');
|
||||
$mform->display();
|
||||
|
||||
// Render table.
|
||||
echo $output->render($table);
|
||||
|
||||
$event = \gradereport_history\event\grade_report_viewed::create(
|
||||
array(
|
||||
'context' => $context,
|
||||
'courseid' => $courseid
|
||||
)
|
||||
);
|
||||
$event->trigger();
|
||||
|
||||
echo $OUTPUT->footer();
|
57
grade/report/history/lang/en/gradereport_history.php
Normal file
57
grade/report/history/lang/en/gradereport_history.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Strings for component 'gradereport_history', language 'en'
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['allgradeitems'] = 'All grade items';
|
||||
$string['allgraders'] = 'All graders';
|
||||
$string['datefrom'] = 'Date from';
|
||||
$string['datetill'] = 'Date till';
|
||||
$string['datetime'] = 'Date and time';
|
||||
$string['deleteditemid'] = 'Delete item with id {$a}';
|
||||
$string['errajaxsearch'] = 'Error when searching users';
|
||||
$string['eventgradereportviewed'] = 'Grade history report viewed';
|
||||
$string['excluded'] = 'Excluded from calculations';
|
||||
$string['foundoneuser'] = '1 user found';
|
||||
$string['foundnusers'] = '{$a} users found';
|
||||
$string['feedbacktext'] = 'Feedback text';
|
||||
$string['finishselectingusers'] = 'Finish selecting users';
|
||||
$string['gradenew'] = 'Revised grade';
|
||||
$string['gradeold'] = 'Original grade';
|
||||
$string['grader'] = 'Grader';
|
||||
$string['history:view'] = 'View the grade history';
|
||||
$string['historyperpage'] = 'History entries per page';
|
||||
$string['historyperpage_help'] = 'This setting determines the number of history entries displayed per page in the history report.';
|
||||
$string['loadmoreusers'] = 'Load more users...';
|
||||
$string['pluginname'] = 'Grade history';
|
||||
$string['preferences'] = 'Grade history preferences';
|
||||
$string['revisedonly'] = 'Revised grades only';
|
||||
$string['revisedonly_help'] = 'Only show grades which have been revised.
|
||||
|
||||
This means only entries which result in the grade changing are listed.';
|
||||
$string['selectuser'] = 'Select user';
|
||||
$string['selectusers'] = 'Select users';
|
||||
$string['selectedusers'] = 'Selected users';
|
||||
$string['source'] = 'Source';
|
||||
$string['useractivitygrade'] = '{$a} grade';
|
||||
$string['useractivityfeedback'] = '{$a} feedback';
|
36
grade/report/history/settings.php
Normal file
36
grade/report/history/settings.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Defines site config settings for the grade history report
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2014 NetSpot Pty Ltd
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
if ($ADMIN->fulltree) {
|
||||
|
||||
// Add settings for this module to the $settings object (it's already defined).
|
||||
$settings->add(new admin_setting_configtext('grade_report_historyperpage',
|
||||
new lang_string('historyperpage', 'gradereport_history'),
|
||||
new lang_string('historyperpage_help', 'gradereport_history'),
|
||||
50
|
||||
));
|
||||
|
||||
}
|
105
grade/report/history/styles.css
Normal file
105
grade/report/history/styles.css
Normal file
@ -0,0 +1,105 @@
|
||||
/* History */
|
||||
|
||||
.path-grade-report-history div.gradeparent {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.path-grade-report-history .singlebutton div,
|
||||
.path-grade-report-history .singlebutton div input[type="button"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* User Selector */
|
||||
.yui3-gradereport_history_usp-hidden {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.gradereport_history_usp .usp-content {
|
||||
position: relative;
|
||||
}
|
||||
.gradereport_history_usp .usp-ajax-content {
|
||||
overflow: auto;
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
.gradereport_history_usp .usp-ajax-content,
|
||||
.gradereport_history_usp .usp-loading-lightbox {
|
||||
height: 375px;
|
||||
}
|
||||
.gradereport_history_usp .usp-loading-lightbox {
|
||||
background-color: #fff;
|
||||
opacity: .5;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.gradereport_history_usp .usp-loading-lightbox img {
|
||||
margin-top: 100px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gradereport_history_usp .usp-search {
|
||||
text-align: center;
|
||||
}
|
||||
.gradereport_history_usp .usp-user {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
.gradereport_history_usp .usp-user:nth-child(odd) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.gradereport_history_usp .usp-first-added {
|
||||
border-top: 1px solid #bbb;
|
||||
}
|
||||
.gradereport_history_usp .usp-checkbox {
|
||||
text-align: center;
|
||||
float: left;
|
||||
padding: 11px 6px 0 6px;
|
||||
}
|
||||
.gradereport_history_usp .usp-checkbox input[type=checkbox] {
|
||||
margin: 0;
|
||||
}
|
||||
.gradereport_history_usp .usp-picture {
|
||||
margin: 6px 3px 0 3px;
|
||||
float: left;
|
||||
}
|
||||
.gradereport_history_usp .usp-userpicture{
|
||||
cursor: pointer;
|
||||
}
|
||||
.gradereport_history_usp .usp-user .details {
|
||||
margin-left: 67px;
|
||||
padding: 3px 6px 0 6px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.gradereport_history_usp .usp-user .details label {
|
||||
margin: 0;
|
||||
}
|
||||
.gradereport_history_usp .usp-more-results {
|
||||
padding: 5px;
|
||||
border-top: 1px solid #bbb;
|
||||
}
|
||||
.gradereport_history_usp .usp-finish {
|
||||
padding-top: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.gradereport_history_usp .usp-finish input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dir-rtl .gradereport_history_usp .usp-search-results .usp-user {
|
||||
text-align: right;
|
||||
}
|
||||
.dir-rtl .gradereport_history_usp .usp-picture,
|
||||
.dir-rtl .gradereport_history_usp .usp-checkbox {
|
||||
float: right;
|
||||
}
|
||||
.dir-rtl .gradereport_history_usp .usp-user .details {
|
||||
margin-right: 67px;
|
||||
margin-left: 0;
|
||||
}
|
||||
.dir-rtl .gradereport_history_usp input.usp-search-btn {
|
||||
margin-right: 5px;
|
||||
}
|
356
grade/report/history/tests/report_test.php
Normal file
356
grade/report/history/tests/report_test.php
Normal file
@ -0,0 +1,356 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Grade history report test.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2014 Frédéric Massart - FMCorz.net
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Grade history report test class.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2014 Frédéric Massart - FMCorz.net
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class gradereport_history_report_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Create some grades.
|
||||
*/
|
||||
public function test_query_db() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Making the setup.
|
||||
$c1 = $this->getDataGenerator()->create_course();
|
||||
$c2 = $this->getDataGenerator()->create_course();
|
||||
$c1ctx = context_course::instance($c1->id);
|
||||
$c2ctx = context_course::instance($c2->id);
|
||||
|
||||
// Users.
|
||||
$u1 = $this->getDataGenerator()->create_user();
|
||||
$u2 = $this->getDataGenerator()->create_user();
|
||||
$u3 = $this->getDataGenerator()->create_user();
|
||||
$u4 = $this->getDataGenerator()->create_user();
|
||||
$u5 = $this->getDataGenerator()->create_user();
|
||||
$grader1 = $this->getDataGenerator()->create_user();
|
||||
$grader2 = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Modules.
|
||||
$c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
|
||||
$c1m2 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
|
||||
$c1m3 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
|
||||
$c2m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
|
||||
$c2m2 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
|
||||
|
||||
// Creating fake history data.
|
||||
$giparams = array('itemtype' => 'mod', 'itemmodule' => 'assign');
|
||||
$grades = array();
|
||||
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c1m1->id));
|
||||
$grades['c1m1u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'timemodified' => time() - 3600));
|
||||
$grades['c1m1u2'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id,
|
||||
'timemodified' => time() + 3600));
|
||||
$grades['c1m1u3'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u3->id));
|
||||
$grades['c1m1u4'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u4->id));
|
||||
$grades['c1m1u5'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u5->id));
|
||||
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c1m2->id));
|
||||
$grades['c1m2u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id));
|
||||
$grades['c1m2u2'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id));
|
||||
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c1m3->id));
|
||||
$grades['c1m3u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id));
|
||||
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c2m1->id));
|
||||
$grades['c2m1u1'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'usermodified' => $grader1->id));
|
||||
$grades['c2m1u2'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id,
|
||||
'usermodified' => $grader1->id));
|
||||
$grades['c2m1u3'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u3->id,
|
||||
'usermodified' => $grader1->id));
|
||||
$grades['c2m1u4'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u4->id,
|
||||
'usermodified' => $grader2->id));
|
||||
|
||||
// Histories where grades have not been revised..
|
||||
$grades['c2m1u5a'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u5->id,
|
||||
'timemodified' => time() - 60));
|
||||
$grades['c2m1u5b'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u5->id,
|
||||
'timemodified' => time()));
|
||||
$grades['c2m1u5c'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u5->id,
|
||||
'timemodified' => time() + 60));
|
||||
|
||||
// Histories where grades have been revised and not revised.
|
||||
$now = time();
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c2m2->id));
|
||||
$grades['c2m2u1a'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'timemodified' => $now - 60, 'finalgrade' => 50));
|
||||
$grades['c2m2u1b'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'timemodified' => $now - 50, 'finalgrade' => 50)); // Not revised.
|
||||
$grades['c2m2u1c'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'timemodified' => $now, 'finalgrade' => 75));
|
||||
$grades['c2m2u1d'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'timemodified' => $now + 10, 'finalgrade' => 75)); // Not revised.
|
||||
$grades['c2m2u1e'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'timemodified' => $now + 60, 'finalgrade' => 25));
|
||||
$grades['c2m2u1f'] = $this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id,
|
||||
'timemodified' => $now + 70, 'finalgrade' => 25)); // Not revised.
|
||||
|
||||
// TODO MDL-46736 Handle deleted/non-existing grade items.
|
||||
// Histories with missing grade items, considered as deleted.
|
||||
// $grades['c2x1u5'] = $this->create_grade_history($giparams + array('itemid' => -1, 'userid' => $u5->id, 'courseid' => $c1->id));
|
||||
// $grades['c2x2u5'] = $this->create_grade_history($giparams + array('itemid' => 999999, 'userid' => $u5->id, 'courseid' => $c1->id));
|
||||
|
||||
// Basic filtering based on course id.
|
||||
$this->assertEquals(8, $this->get_tablelog_results($c1ctx, array(), true));
|
||||
$this->assertEquals(13, $this->get_tablelog_results($c2ctx, array(), true));
|
||||
|
||||
// Filtering on 1 user.
|
||||
$this->assertEquals(3, $this->get_tablelog_results($c1ctx, array('userids' => $u1->id), true));
|
||||
|
||||
// Filtering on more users.
|
||||
$this->assertEquals(4, $this->get_tablelog_results($c1ctx, array('userids' => "$u1->id,$u3->id"), true));
|
||||
|
||||
// Filtering based on one grade item.
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c1m1->id));
|
||||
$this->assertEquals(5, $this->get_tablelog_results($c1ctx, array('itemid' => $gi->id), true));
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c1m3->id));
|
||||
$this->assertEquals(1, $this->get_tablelog_results($c1ctx, array('itemid' => $gi->id), true));
|
||||
|
||||
// Filtering based on the grader.
|
||||
$this->assertEquals(3, $this->get_tablelog_results($c2ctx, array('grader' => $grader1->id), true));
|
||||
$this->assertEquals(1, $this->get_tablelog_results($c2ctx, array('grader' => $grader2->id), true));
|
||||
|
||||
// Filtering based on date.
|
||||
$results = $this->get_tablelog_results($c1ctx, array('datefrom' => time() + 1800));
|
||||
$this->assertGradeHistoryIds(array($grades['c1m1u2']->id), $results);
|
||||
$results = $this->get_tablelog_results($c1ctx, array('datetill' => time() - 1800));
|
||||
$this->assertGradeHistoryIds(array($grades['c1m1u1']->id), $results);
|
||||
$results = $this->get_tablelog_results($c1ctx, array('datefrom' => time() - 1800, 'datetill' => time() + 1800));
|
||||
$this->assertGradeHistoryIds(array($grades['c1m1u3']->id, $grades['c1m1u4']->id, $grades['c1m1u5']->id,
|
||||
$grades['c1m2u1']->id, $grades['c1m2u2']->id, $grades['c1m3u1']->id), $results);
|
||||
|
||||
// Filtering based on revised only.
|
||||
$this->assertEquals(3, $this->get_tablelog_results($c2ctx, array('userids' => $u5->id), true));
|
||||
$this->assertEquals(1, $this->get_tablelog_results($c2ctx, array('userids' => $u5->id, 'revisedonly' => true), true));
|
||||
|
||||
// More filtering based on revised only.
|
||||
$gi = grade_item::fetch($giparams + array('iteminstance' => $c2m2->id));
|
||||
$this->assertEquals(6, $this->get_tablelog_results($c2ctx, array('userids' => $u1->id, 'itemid' => $gi->id), true));
|
||||
$results = $this->get_tablelog_results($c2ctx, array('userids' => $u1->id, 'itemid' => $gi->id, 'revisedonly' => true));
|
||||
$this->assertGradeHistoryIds(array($grades['c2m2u1a']->id, $grades['c2m2u1c']->id, $grades['c2m2u1e']->id), $results);
|
||||
|
||||
// Checking the value of the previous grade.
|
||||
$this->assertEquals(null, $results[$grades['c2m2u1a']->id]->prevgrade);
|
||||
$this->assertEquals($grades['c2m2u1a']->finalgrade, $results[$grades['c2m2u1c']->id]->prevgrade);
|
||||
$this->assertEquals($grades['c2m2u1c']->finalgrade, $results[$grades['c2m2u1e']->id]->prevgrade);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get users helper method.
|
||||
*/
|
||||
public function test_get_users() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Making the setup.
|
||||
$c1 = $this->getDataGenerator()->create_course();
|
||||
$c2 = $this->getDataGenerator()->create_course();
|
||||
$c1ctx = context_course::instance($c1->id);
|
||||
$c2ctx = context_course::instance($c2->id);
|
||||
|
||||
$c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
|
||||
$c2m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
|
||||
|
||||
// Users.
|
||||
$u1 = $this->getDataGenerator()->create_user(array('firstname' => 'Eric', 'lastname' => 'Cartman'));
|
||||
$u2 = $this->getDataGenerator()->create_user(array('firstname' => 'Stan', 'lastname' => 'Marsh'));
|
||||
$u3 = $this->getDataGenerator()->create_user(array('firstname' => 'Kyle', 'lastname' => 'Broflovski'));
|
||||
$u4 = $this->getDataGenerator()->create_user(array('firstname' => 'Kenny', 'lastname' => 'McCormick'));
|
||||
|
||||
// Creating grade history for some users.
|
||||
$gi = grade_item::fetch(array('iteminstance' => $c1m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u2->id));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u3->id));
|
||||
|
||||
$gi = grade_item::fetch(array('iteminstance' => $c2m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u4->id));
|
||||
|
||||
// Checking fetching some users.
|
||||
$users = \gradereport_history\helper::get_users($c1ctx);
|
||||
$this->assertCount(3, $users);
|
||||
$this->assertArrayHasKey($u3->id, $users);
|
||||
$users = \gradereport_history\helper::get_users($c2ctx);
|
||||
$this->assertCount(1, $users);
|
||||
$this->assertArrayHasKey($u4->id, $users);
|
||||
$users = \gradereport_history\helper::get_users($c1ctx, 'c');
|
||||
$this->assertCount(1, $users);
|
||||
$this->assertArrayHasKey($u1->id, $users);
|
||||
$users = \gradereport_history\helper::get_users($c1ctx, '', 0, 2);
|
||||
$this->assertCount(2, $users);
|
||||
$this->assertArrayHasKey($u3->id, $users);
|
||||
$this->assertArrayHasKey($u1->id, $users);
|
||||
$users = \gradereport_history\helper::get_users($c1ctx, '', 1, 2);
|
||||
$this->assertCount(1, $users);
|
||||
$this->assertArrayHasKey($u2->id, $users);
|
||||
|
||||
// Checking the count of users.
|
||||
$this->assertEquals(3, \gradereport_history\helper::get_users_count($c1ctx));
|
||||
$this->assertEquals(1, \gradereport_history\helper::get_users_count($c2ctx));
|
||||
$this->assertEquals(1, \gradereport_history\helper::get_users_count($c1ctx, 'c'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get graders helper method.
|
||||
*/
|
||||
public function test_graders() {
|
||||
$this->resetAfterTest();
|
||||
|
||||
// Making the setup.
|
||||
$c1 = $this->getDataGenerator()->create_course();
|
||||
$c2 = $this->getDataGenerator()->create_course();
|
||||
|
||||
$c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
|
||||
$c2m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
|
||||
|
||||
// Users.
|
||||
$u1 = $this->getDataGenerator()->create_user(array('firstname' => 'Eric', 'lastname' => 'Cartman'));
|
||||
$u2 = $this->getDataGenerator()->create_user(array('firstname' => 'Stan', 'lastname' => 'Marsh'));
|
||||
$u3 = $this->getDataGenerator()->create_user(array('firstname' => 'Kyle', 'lastname' => 'Broflovski'));
|
||||
$u4 = $this->getDataGenerator()->create_user(array('firstname' => 'Kenny', 'lastname' => 'McCormick'));
|
||||
|
||||
// Creating grade history for some users.
|
||||
$gi = grade_item::fetch(array('iteminstance' => $c1m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u1->id));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u2->id));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u3->id));
|
||||
|
||||
$gi = grade_item::fetch(array('iteminstance' => $c2m1->id, 'itemtype' => 'mod', 'itemmodule' => 'assign'));
|
||||
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u1->id, 'usermodified' => $u4->id));
|
||||
|
||||
// Checking fetching some users.
|
||||
$graders = \gradereport_history\helper::get_graders($c1->id);
|
||||
$this->assertCount(4, $graders); // Including "all graders" .
|
||||
$this->assertArrayHasKey($u1->id, $graders);
|
||||
$this->assertArrayHasKey($u2->id, $graders);
|
||||
$this->assertArrayHasKey($u3->id, $graders);
|
||||
$graders = \gradereport_history\helper::get_graders($c2->id);
|
||||
$this->assertCount(2, $graders); // Including "all graders" .
|
||||
$this->assertArrayHasKey($u4->id, $graders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the array of grade objects contains exactly the right IDs.
|
||||
*
|
||||
* @param array $expectedids Array of expected IDs.
|
||||
* @param array $objects Array of objects returned by the table.
|
||||
*/
|
||||
protected function assertGradeHistoryIds(array $expectedids, array $objects) {
|
||||
$this->assertCount(count($expectedids), $objects);
|
||||
$expectedids = array_flip($expectedids);
|
||||
foreach ($objects as $object) {
|
||||
$this->assertArrayHasKey($object->id, $expectedids);
|
||||
unset($expectedids[$object->id]);
|
||||
}
|
||||
$this->assertCount(0, $expectedids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new grade history entry.
|
||||
*
|
||||
* @param array $params Of values.
|
||||
* @return object The grade object.
|
||||
*/
|
||||
protected function create_grade_history($params) {
|
||||
global $DB;
|
||||
$params = (array) $params;
|
||||
|
||||
if (!isset($params['itemid'])) {
|
||||
throw new coding_exception('Missing itemid key.');
|
||||
}
|
||||
if (!isset($params['userid'])) {
|
||||
throw new coding_exception('Missing userid key.');
|
||||
}
|
||||
|
||||
// Default object.
|
||||
$grade = new stdClass();
|
||||
$grade->itemid = 0;
|
||||
$grade->userid = 0;
|
||||
$grade->oldid = 123;
|
||||
$grade->rawgrade = 50;
|
||||
$grade->finalgrade = 50;
|
||||
$grade->timecreated = time();
|
||||
$grade->timemodified = time();
|
||||
$grade->information = '';
|
||||
$grade->informationformat = FORMAT_PLAIN;
|
||||
$grade->feedback = '';
|
||||
$grade->feedbackformat = FORMAT_PLAIN;
|
||||
$grade->usermodified = 2;
|
||||
|
||||
// Merge with data passed.
|
||||
$grade = (object) array_merge((array) $grade, $params);
|
||||
|
||||
// Insert record.
|
||||
$grade->id = $DB->insert_record('grade_grades_history', $grade);
|
||||
|
||||
return $grade;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a table log object.
|
||||
*
|
||||
* @param context_course $coursecontext The course context.
|
||||
* @param array $filters An array of filters.
|
||||
* @param boolean $count When true, returns a count rather than an array of objects.
|
||||
* @return mixed Count or array of objects.
|
||||
*/
|
||||
protected function get_tablelog_results($coursecontext, $filters = array(), $count = false) {
|
||||
$table = new gradereport_history_tests_tablelog('something', $coursecontext, new moodle_url(''), $filters);
|
||||
return $table->get_test_results($count);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended table log class.
|
||||
*/
|
||||
class gradereport_history_tests_tablelog extends \gradereport_history\output\tablelog {
|
||||
|
||||
/**
|
||||
* Get the test results.
|
||||
*
|
||||
* @param boolean $count Whether or not we want the count.
|
||||
* @return mixed Count or array of objects.
|
||||
*/
|
||||
public function get_test_results($count = false) {
|
||||
global $DB;
|
||||
if ($count) {
|
||||
list($sql, $params) = $this->get_sql_and_params(true);
|
||||
return $DB->count_records_sql($sql, $params);
|
||||
} else {
|
||||
$this->setup();
|
||||
list($sql, $params) = $this->get_sql_and_params();
|
||||
return $DB->get_records_sql($sql, $params);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
75
grade/report/history/users_ajax.php
Normal file
75
grade/report/history/users_ajax.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* User searching requests.
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
define('AJAX_SCRIPT', true);
|
||||
|
||||
require_once(__DIR__ . '/../../../config.php');
|
||||
|
||||
$id = required_param('id', PARAM_INT); // Course id.
|
||||
$search = optional_param('search', '', PARAM_RAW);
|
||||
$page = optional_param('page', 0, PARAM_INT);
|
||||
|
||||
$course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST);
|
||||
$context = context_course::instance($course->id, MUST_EXIST);
|
||||
|
||||
if ($course->id == SITEID) {
|
||||
throw new moodle_exception('invalidcourse');
|
||||
}
|
||||
|
||||
require_sesskey();
|
||||
require_login($course);
|
||||
require_capability('gradereport/history:view', $context);
|
||||
require_capability('moodle/grade:viewall', $context);
|
||||
|
||||
$outcome = new stdClass();
|
||||
$outcome->success = true;
|
||||
$outcome->error = '';
|
||||
|
||||
$users = \gradereport_history\helper::get_users($context, $search, $page, 25);
|
||||
$outcome->response = array('users' => array());
|
||||
$outcome->response['totalusers'] = \gradereport_history\helper::get_users_count($context, $search);;
|
||||
|
||||
$extrafields = get_extra_user_fields($context);
|
||||
$useroptions = array('link' => false, 'visibletoscreenreaders' => false);
|
||||
|
||||
// Format the user record.
|
||||
foreach ($users as $user) {
|
||||
$newuser = new stdClass();
|
||||
$newuser->userid = $user->id;
|
||||
$newuser->picture = $OUTPUT->user_picture($user, $useroptions);
|
||||
$newuser->fullname = fullname($user);
|
||||
$fieldvalues = array();
|
||||
foreach ($extrafields as $field) {
|
||||
$fieldvalues[] = s($user->{$field});
|
||||
}
|
||||
$newuser->extrafields = implode(', ', $fieldvalues);
|
||||
$outcome->response['users'][] = $newuser;
|
||||
}
|
||||
|
||||
$outcome->success = true;
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo json_encode($outcome);
|
||||
echo $OUTPUT->footer();
|
30
grade/report/history/version.php
Normal file
30
grade/report/history/version.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Version details for the grade history
|
||||
*
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @author Adam Olley <adam.olley@netspot.com.au>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2014072900;
|
||||
$plugin->requires = 2014072400;
|
||||
$plugin->component = 'gradereport_history';
|
@ -0,0 +1,866 @@
|
||||
YUI.add('moodle-gradereport_history-userselector', function (Y, NAME) {
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The User Selector for the grade history report.
|
||||
*
|
||||
* @module moodle-gradereport_history-userselector
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @main moodle-gradereport_history-userselector
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module moodle-gradereport_history-userselector
|
||||
*/
|
||||
|
||||
var COMPONENT = 'gradereport_history';
|
||||
var USP = {
|
||||
AJAXURL: 'ajaxurl',
|
||||
BASE: 'base',
|
||||
CHECKBOX_NAME_PREFIX: 'usp-u',
|
||||
COURSEID: 'courseid',
|
||||
DIALOGUE_PREFIX: 'moodle-dialogue',
|
||||
NAME: 'gradereport_history_usp',
|
||||
PAGE: 'page',
|
||||
PARAMS: 'params',
|
||||
PERPAGE: 'perPage',
|
||||
SEARCH: 'search',
|
||||
SEARCHBTN: 'searchbtn',
|
||||
SELECTEDUSERS: 'selectedUsers',
|
||||
URL: 'url',
|
||||
USERCOUNT: 'userCount'
|
||||
};
|
||||
var CSS = {
|
||||
ACCESSHIDE: 'accesshide',
|
||||
AJAXCONTENT: 'usp-ajax-content',
|
||||
CHECKBOX: 'usp-checkbox',
|
||||
CLOSE: 'close',
|
||||
CLOSEBTN: 'usp-finish',
|
||||
CONTENT: 'usp-content',
|
||||
DETAILS: 'details',
|
||||
EXTRAFIELDS: 'extrafields',
|
||||
FIRSTADDED: 'usp-first-added',
|
||||
FULLNAME: 'fullname',
|
||||
HEADER: 'usp-header',
|
||||
HIDDEN: 'hidden',
|
||||
LIGHTBOX: 'usp-loading-lightbox',
|
||||
LOADINGICON: 'loading-icon',
|
||||
MORERESULTS: 'usp-more-results',
|
||||
OPTIONS: 'options',
|
||||
PICTURE: 'usp-picture',
|
||||
RESULTSCOUNT: 'usp-results-count',
|
||||
SEARCH: 'usp-search',
|
||||
SEARCHBTN: 'usp-search-btn',
|
||||
SEARCHFIELD: 'usp-search-field',
|
||||
SEARCHRESULTS: 'usp-search-results',
|
||||
SELECTED: 'selected',
|
||||
USER: 'usp-user',
|
||||
USERS: 'usp-users',
|
||||
WRAP: 'usp-wrap'
|
||||
};
|
||||
var SELECTORS = {
|
||||
AJAXCONTENT: '.' + CSS.AJAXCONTENT,
|
||||
FINISHBTN: '.' + CSS.CLOSEBTN + ' input',
|
||||
FIRSTADDED: '.' + CSS.FIRSTADDED,
|
||||
FULLNAME: '.' + CSS.FULLNAME + ' label',
|
||||
LIGHTBOX: '.' + CSS.LIGHTBOX,
|
||||
MORERESULTS: '.' + CSS.MORERESULTS,
|
||||
OPTIONS: '.' + CSS.OPTIONS,
|
||||
PICTURE: '.' + CSS.USER + ' .userpicture',
|
||||
RESULTSCOUNT: '.' + CSS.RESULTSCOUNT,
|
||||
RESULTSUSERS: '.' + CSS.SEARCHRESULTS + ' .' + CSS.USERS,
|
||||
SEARCHBTN: '.' + CSS.SEARCHBTN,
|
||||
SEARCHFIELD: '.' + CSS.SEARCHFIELD,
|
||||
SELECTEDNAMES: '.felement .selectednames',
|
||||
TRIGGER: '.gradereport_history_plugin input.selectortrigger',
|
||||
USER: '.' + CSS.USER,
|
||||
USERFULLNAMES: 'input[name="userfullnames"]',
|
||||
USERIDS: 'input[name="userids"]',
|
||||
USERSELECT: '.' + CSS.CHECKBOX + ' input[type=checkbox]'
|
||||
};
|
||||
|
||||
/**
|
||||
* User Selector.
|
||||
*
|
||||
* @namespace M.gradereport_history
|
||||
* @class UserSelector
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
var USERSELECTOR = function() {
|
||||
USERSELECTOR.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.core.dialogue, {
|
||||
|
||||
/**
|
||||
* Whether or not this is the first time the user displays the dialogue within that request.
|
||||
*
|
||||
* @property _firstDisplay
|
||||
* @type Boolean
|
||||
* @private
|
||||
*/
|
||||
_firstDisplay: true,
|
||||
|
||||
/**
|
||||
* The list of all the users selected while the dialogue is open.
|
||||
*
|
||||
* @type Object
|
||||
* @property _usersBufferList
|
||||
* @private
|
||||
*/
|
||||
_usersBufferList: null,
|
||||
|
||||
/**
|
||||
* The Node on which the focus is set.
|
||||
*
|
||||
* @property _userTabFocus
|
||||
* @type Node
|
||||
* @private
|
||||
*/
|
||||
_userTabFocus: null,
|
||||
|
||||
/**
|
||||
* Compiled template function for a user node.
|
||||
*
|
||||
* @property _userTemplate
|
||||
* @type Function
|
||||
* @private
|
||||
*/
|
||||
_userTemplate: null,
|
||||
|
||||
initializer: function() {
|
||||
var bb = this.get('boundingBox'),
|
||||
content,
|
||||
params,
|
||||
tpl;
|
||||
|
||||
tpl = Y.Handlebars.compile(
|
||||
'<div class="{{CSS.WRAP}}">' +
|
||||
'<div class="{{CSS.HEADER}}">' +
|
||||
'<div class="{{CSS.SEARCH}}" role="search">' +
|
||||
'<form>' +
|
||||
'<input type="text" class="{{CSS.SEARCHFIELD}}" ' +
|
||||
'aria-label="{{get_string "search" "moodle"}}" value="" />' +
|
||||
'<input type="submit" class="{{CSS.SEARCHBTN}}"' +
|
||||
'value="{{get_string "search" "moodle"}}">' +
|
||||
'</form>' +
|
||||
'<div aria-live="polite" class="{{CSS.RESULTSCOUNT}}">{{get_string "loading" "admin"}}</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.CONTENT}}">' +
|
||||
'<form>' +
|
||||
'<div class="{{CSS.AJAXCONTENT}}" aria-live="polite"></div>' +
|
||||
'<div class="{{CSS.LIGHTBOX}} {{CSS.HIDDEN}}">' +
|
||||
'<img class="{{CSS.LOADINGICON}}" alt="{{get_string "loading" "admin"}}"' +
|
||||
'src="{{{loadingIcon}}}">' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.CLOSEBTN}}">' +
|
||||
'<input type="submit" value="{{get_string "finishselectingusers" COMPONENT}}">' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'</div>' +
|
||||
'</div>');
|
||||
|
||||
content = Y.Node.create(
|
||||
tpl({
|
||||
COMPONENT: COMPONENT,
|
||||
CSS: CSS,
|
||||
loadingIcon: M.util.image_url('i/loading', 'moodle')
|
||||
})
|
||||
);
|
||||
|
||||
// Set the title and content.
|
||||
this.getStdModNode(Y.WidgetStdMod.HEADER).prepend(Y.Node.create('<h1>' + this.get('title') + '</h1>'));
|
||||
this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
|
||||
|
||||
// Use standard dialogue class name. This removes the default styling of the footer.
|
||||
this.get('boundingBox').one('.moodle-dialogue-wrap').addClass('moodle-dialogue-content');
|
||||
|
||||
// Add the event on the button that opens the dialogue.
|
||||
Y.one(SELECTORS.TRIGGER).on('click', this.show, this);
|
||||
|
||||
// The button to finalize the selection.
|
||||
bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this);
|
||||
|
||||
// Delegate the keyboard navigation in the users list.
|
||||
bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this);
|
||||
|
||||
// Delegate the action to select a user.
|
||||
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this);
|
||||
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this);
|
||||
|
||||
params = this.get(USP.PARAMS);
|
||||
params.id = this.get(USP.COURSEID);
|
||||
this.set(USP.PARAMS, params);
|
||||
|
||||
bb.one(SELECTORS.SEARCHBTN).on('click', this.search, this, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the dialogue.
|
||||
*
|
||||
* @method show
|
||||
*/
|
||||
show: function(e) {
|
||||
var bb;
|
||||
this._usersBufferList = Y.clone(this.get(USP.SELECTEDUSERS));
|
||||
if (this._firstDisplay) {
|
||||
// Load the default list of users when the dialogue is loaded for the first time.
|
||||
this._firstDisplay = false;
|
||||
this.search(e, false);
|
||||
} else {
|
||||
// Leave the content as is, but reset the selection.
|
||||
bb = this.get('boundingBox');
|
||||
|
||||
// Remove all the selected users.
|
||||
bb.all(SELECTORS.USER).each(function(node) {
|
||||
this.markUserNode(node, false);
|
||||
}, this);
|
||||
|
||||
// Select the users.
|
||||
Y.Object.each(this._usersBufferList, function(v, k) {
|
||||
var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]');
|
||||
if (user) {
|
||||
this.markUserNode(user, true);
|
||||
}
|
||||
}, this);
|
||||
|
||||
// Reset the tab focus.
|
||||
this.setUserTabFocus(bb.one(SELECTORS.USER));
|
||||
}
|
||||
return Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Search for users.
|
||||
*
|
||||
* @method search
|
||||
* @param {EventFacade} e The event.
|
||||
* @param {Boolean} append Whether we want to append the results to the current results or not.
|
||||
*/
|
||||
search: function(e, append) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
var params;
|
||||
if (append) {
|
||||
this.set(USP.PAGE, this.get(USP.PAGE)+1);
|
||||
} else {
|
||||
this.set(USP.USERCOUNT, 0);
|
||||
this.set(USP.PAGE, 0);
|
||||
}
|
||||
params = this.get(USP.PARAMS);
|
||||
params.sesskey = M.cfg.sesskey;
|
||||
params.action = 'searchusers';
|
||||
params.search = this.get('boundingBox').one(SELECTORS.SEARCHFIELD).get('value');
|
||||
params.page = this.get(USP.PAGE);
|
||||
params.perpage = this.get(USP.PERPAGE);
|
||||
|
||||
Y.io(M.cfg.wwwroot + this.get(USP.AJAXURL), {
|
||||
method:'POST',
|
||||
data:build_querystring(params),
|
||||
on: {
|
||||
start: this.preSearch,
|
||||
complete: this.processSearchResults,
|
||||
end: this.postSearch
|
||||
},
|
||||
context:this,
|
||||
"arguments": { // Quoted because this is a reserved keyword.
|
||||
append: append
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Pre search callback.
|
||||
*
|
||||
* @method preSearch
|
||||
* @param {String} transactionId The transaction ID.
|
||||
* @param {Object} args The arguments passed from YUI.io()
|
||||
*/
|
||||
preSearch: function(unused, args) {
|
||||
var bb = this.get('boundingBox');
|
||||
|
||||
// Display the lightbox.
|
||||
bb.one(SELECTORS.LIGHTBOX).removeClass(CSS.HIDDEN);
|
||||
|
||||
// Set the number of results to 'loading...'.
|
||||
if (!args.append) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('loading', 'admin'));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Post search callback.
|
||||
*
|
||||
* @method postSearch
|
||||
* @param {String} transactionId The transaction ID.
|
||||
* @param {Object} args The arguments passed from YUI.io()
|
||||
*/
|
||||
postSearch: function(transactionId, args) {
|
||||
var bb = this.get('boundingBox'),
|
||||
firstAdded = bb.one(SELECTORS.FIRSTADDED),
|
||||
firstUser;
|
||||
|
||||
// Hide the lightbox.
|
||||
bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN);
|
||||
|
||||
if (args.append && firstAdded) {
|
||||
// Sets the focus on the newly added user if we are appending results.
|
||||
this.setUserTabFocus(firstAdded);
|
||||
firstAdded.one(SELECTORS.USERSELECT).focus();
|
||||
} else {
|
||||
// New search result, set the tab focus on the first user returned.
|
||||
firstUser = bb.one(SELECTORS.USER);
|
||||
if (firstUser) {
|
||||
this.setUserTabFocus(firstUser);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process and display the search results.
|
||||
*
|
||||
* @method processSearchResults
|
||||
* @param {String} tid The transaction ID.
|
||||
* @param {Object} outcome The response object.
|
||||
* @param {Object} args The arguments passed from YUI.io().
|
||||
*/
|
||||
processSearchResults: function(tid, outcome, args) {
|
||||
var result = false,
|
||||
error = false,
|
||||
bb = this.get('boundingBox'),
|
||||
users,
|
||||
userTemplate,
|
||||
count,
|
||||
selected,
|
||||
i,
|
||||
firstAdded = true,
|
||||
node,
|
||||
content,
|
||||
fetchmore,
|
||||
totalUsers;
|
||||
|
||||
// Decodes the result.
|
||||
try {
|
||||
result = Y.JSON.parse(outcome.responseText);
|
||||
if (!result.success || result.error) {
|
||||
error = true;
|
||||
}
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
// There was an error.
|
||||
if (error) {
|
||||
this.setContent('');
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('errajaxsearch', COMPONENT));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the div containing the users when it is a fresh search.
|
||||
if (!args.append) {
|
||||
users = Y.Node.create('<div role="listbox" aria-activedescendant="" aria-multiselectable="true" class="'+CSS.USERS+'"></div>');
|
||||
} else {
|
||||
users = bb.one(SELECTORS.RESULTSUSERS);
|
||||
}
|
||||
|
||||
// Compile the template for each user node.
|
||||
if (!this._userTemplate) {
|
||||
this._userTemplate = Y.Handlebars.compile(
|
||||
'<div role="option" aria-selected="false" class="{{CSS.USER}} clearfix" ' +
|
||||
'data-userid="{{userId}}">' +
|
||||
'<div class="{{CSS.CHECKBOX}}">' +
|
||||
'<input name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox" tabindex="-1"' +
|
||||
'id="{{checkboxId}}" aria-describedby="{{checkboxId}} {{extraFieldsId}}"/>' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.PICTURE}}">{{{picture}}}</div>' +
|
||||
'<div class="{{CSS.DETAILS}}">' +
|
||||
'<div class="{{CSS.FULLNAME}}">' +
|
||||
'<label for="{{checkboxId}}">{{fullname}}</label>' +
|
||||
'</div>' +
|
||||
'<div id="{{extraFieldsId}}" class="{{CSS.EXTRAFIELDS}}">{{extrafields}}</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
userTemplate = this._userTemplate;
|
||||
|
||||
// Append the users one by one.
|
||||
count = this.get(USP.USERCOUNT);
|
||||
selected = '';
|
||||
for (i in result.response.users) {
|
||||
count++;
|
||||
user = result.response.users[i];
|
||||
|
||||
// If already selected.
|
||||
if (Y.Object.hasKey(this._usersBufferList, user.userid)) {
|
||||
selected = true;
|
||||
} else {
|
||||
selected = false;
|
||||
}
|
||||
|
||||
node = Y.Node.create(userTemplate({
|
||||
checkboxId: Y.guid(),
|
||||
COMPONENT: COMPONENT,
|
||||
count: count,
|
||||
CSS: CSS,
|
||||
extrafields: user.extrafields,
|
||||
extraFieldsId: Y.guid(),
|
||||
fullname: user.fullname,
|
||||
picture: user.picture,
|
||||
userId: user.userid,
|
||||
USP: USP
|
||||
}));
|
||||
|
||||
this.markUserNode(node, selected);
|
||||
|
||||
// Noting the first user that was when adding more results.
|
||||
if (args.append && firstAdded) {
|
||||
users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED);
|
||||
node.addClass(CSS.FIRSTADDED);
|
||||
firstAdded = false;
|
||||
}
|
||||
users.append(node);
|
||||
}
|
||||
this.set(USP.USERCOUNT, count);
|
||||
|
||||
// Update the count of users, and add a button to load more if need be.
|
||||
totalUsers = parseInt(result.response.totalusers, 10);
|
||||
if (!args.append) {
|
||||
if (totalUsers === 0) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('noresults', 'moodle'));
|
||||
content = '';
|
||||
} else {
|
||||
if (totalUsers === 1) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundoneuser', COMPONENT));
|
||||
} else {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundnusers', COMPONENT, totalUsers));
|
||||
}
|
||||
|
||||
content = Y.Node.create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
|
||||
.append(users);
|
||||
if (result.response.totalusers > (this.get(USP.PAGE)+1)*this.get(USP.PERPAGE)) {
|
||||
fetchmore = Y.Node.create('<div class="'+CSS.MORERESULTS+'">' +
|
||||
'<a href="#" role="button">'+M.util.get_string('loadmoreusers', COMPONENT)+'</a></div>');
|
||||
fetchmore.one('a').on('click', this.search, this, true);
|
||||
fetchmore.one('a').on('key', this.search, 'space', this, true);
|
||||
content.append(fetchmore);
|
||||
}
|
||||
}
|
||||
this.setContent(content);
|
||||
} else {
|
||||
if (totalUsers <= (this.get(USP.PAGE)+1)*this.get(USP.PERPAGE)) {
|
||||
bb.one(SELECTORS.MORERESULTS).remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user has finished selecting users.
|
||||
*
|
||||
* @method finishSelectingUsers
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
finishSelectingUsers: function(e) {
|
||||
e.preventDefault();
|
||||
this.applySelection();
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply the selection made.
|
||||
*
|
||||
* @method applySelection
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
applySelection: function() {
|
||||
var userIds = Y.Object.keys(this._usersBufferList);
|
||||
this.set(USP.SELECTEDUSERS, Y.clone(this._usersBufferList))
|
||||
.setNameDisplay();
|
||||
Y.one(SELECTORS.USERIDS).set('value', userIds.join());
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a user.
|
||||
*
|
||||
* @method SelectUser
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
selectUser: function(e) {
|
||||
var user = e.currentTarget.ancestor(SELECTORS.USER),
|
||||
checkbox = user.one(SELECTORS.USERSELECT),
|
||||
fullname = user.one(SELECTORS.FULLNAME).get('innerHTML'),
|
||||
checked = checkbox.get('checked'),
|
||||
userId = user.getData('userid');
|
||||
|
||||
if (e.currentTarget !== checkbox) {
|
||||
// We triggered the selection from another node, so we need to change the checkbox value.
|
||||
checked = !checked;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Selecting the user.
|
||||
this._usersBufferList[userId] = fullname;
|
||||
} else {
|
||||
// De-selecting the user.
|
||||
delete this._usersBufferList[userId];
|
||||
delete this._usersBufferList[parseInt(userId, 10)]; // Also remove numbered keys.
|
||||
}
|
||||
|
||||
this.markUserNode(user, checked);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark a user node as selected or not.
|
||||
*
|
||||
* This only takes care of the DOM side of things, not the internal mechanism
|
||||
* storing what users have been selected or not.
|
||||
*
|
||||
* @param {Node} node The user node.
|
||||
* @param {Boolean} selected True to mark as selected.
|
||||
* @chainable
|
||||
*/
|
||||
markUserNode: function(node, selected) {
|
||||
if (selected) {
|
||||
node.addClass(CSS.SELECTED)
|
||||
.set('aria-selected', true)
|
||||
.one(SELECTORS.USERSELECT)
|
||||
.set('checked', true);
|
||||
} else {
|
||||
node.removeClass(CSS.SELECTED)
|
||||
.set('aria-selected', false)
|
||||
.one(SELECTORS.USERSELECT)
|
||||
.set('checked', false);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the content of the dialogue.
|
||||
*
|
||||
* @method setContent
|
||||
* @param {String} content The content.
|
||||
* @chainable
|
||||
*/
|
||||
setContent: function(content) {
|
||||
this.get('boundingBox').one(SELECTORS.AJAXCONTENT).setHTML(content);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the names of the selected users in the form.
|
||||
*
|
||||
* @method setNameDisplay
|
||||
*/
|
||||
setNameDisplay: function() {
|
||||
var namelist = Y.Object.values(this.get(USP.SELECTEDUSERS));
|
||||
Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', '));
|
||||
Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join());
|
||||
},
|
||||
|
||||
/**
|
||||
* User keyboard navigation.
|
||||
*
|
||||
* @method userKeyboardNavigation
|
||||
*/
|
||||
userKeyboardNavigation: function(e) {
|
||||
var bb = this.get('boundingBox'),
|
||||
users = bb.all(SELECTORS.USER),
|
||||
direction = 1,
|
||||
user,
|
||||
current = e.target.ancestor(SELECTORS.USER, true);
|
||||
|
||||
if (e.keyCode === 38) {
|
||||
direction = -1;
|
||||
}
|
||||
|
||||
user = this.findFocusableUser(users, current, direction);
|
||||
if (user) {
|
||||
e.preventDefault();
|
||||
user.one(SELECTORS.USERSELECT).focus();
|
||||
this.setUserTabFocus(user);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the next or previous focusable node.
|
||||
*
|
||||
* @param {NodeList} users The list of users.
|
||||
* @param {Node} user The user to start with.
|
||||
* @param {Number} direction The direction in which to go.
|
||||
* @return {Node|null} A user node, or null if not found.
|
||||
* @method findFocusableUser
|
||||
*/
|
||||
findFocusableUser: function(users, user, direction) {
|
||||
var index = users.indexOf(user);
|
||||
|
||||
if (users.size() < 1) {
|
||||
Y.log('The users list is empty', 'debug', COMPONENT);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
Y.log('Unable to find the user in the list of users', 'debug', COMPONENT);
|
||||
return users.item(0);
|
||||
}
|
||||
|
||||
index += direction;
|
||||
|
||||
// Wrap the navigation when reaching the top of the bottom.
|
||||
if (index < 0) {
|
||||
index = users.size() - 1;
|
||||
} else if (index >= users.size()) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
return users.item(index);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the user tab focus.
|
||||
*
|
||||
* @param {Node} user The user node.
|
||||
* @method setUserTabFocus
|
||||
*/
|
||||
setUserTabFocus: function(user) {
|
||||
if (this._userTabFocus) {
|
||||
this._userTabFocus.setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
this._userTabFocus = user.one(SELECTORS.USERSELECT);
|
||||
this._userTabFocus.setAttribute('tabindex', '0');
|
||||
|
||||
this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
|
||||
}
|
||||
|
||||
}, {
|
||||
NAME: USP.NAME,
|
||||
CSS_PREFIX: USP.CSS_PREFIX,
|
||||
ATTRS: {
|
||||
|
||||
/**
|
||||
* The header.
|
||||
*
|
||||
* @attribute title
|
||||
* @default selectusers language string.
|
||||
* @type String
|
||||
*/
|
||||
title: {
|
||||
validator: Y.Lang.isString,
|
||||
valueFn: function() {
|
||||
return M.util.get_string('selectusers', COMPONENT);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The current page URL.
|
||||
*
|
||||
* @attribute url
|
||||
* @default null
|
||||
* @type String
|
||||
*/
|
||||
url: {
|
||||
validator: Y.Lang.isString,
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* The URL to the Ajax file.
|
||||
*
|
||||
* @attribute ajaxurl
|
||||
* @default null
|
||||
* @type String
|
||||
*/
|
||||
ajaxurl: {
|
||||
validator: Y.Lang.isString,
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* The names of the selected users.
|
||||
*
|
||||
* The keys are the user IDs, the values are their fullname.
|
||||
*
|
||||
* @attribute selectedUsers
|
||||
* @default null
|
||||
* @type Object
|
||||
*/
|
||||
selectedUsers: {
|
||||
validator: Y.Lang.isObject,
|
||||
value: null,
|
||||
getter: function(v) {
|
||||
if (v === null) {
|
||||
return {};
|
||||
}
|
||||
return v;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The course ID.
|
||||
*
|
||||
* @attribute courseid
|
||||
* @default null
|
||||
* @type Number
|
||||
*/
|
||||
courseid: {
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* Array of parameters.
|
||||
*
|
||||
* @attribute params
|
||||
* @default []
|
||||
* @type Array
|
||||
*/
|
||||
params: {
|
||||
validator: Y.Lang.isArray,
|
||||
value: []
|
||||
},
|
||||
|
||||
/**
|
||||
* The page we are on.
|
||||
*
|
||||
* @attribute page
|
||||
* @default 0
|
||||
* @type Number
|
||||
*/
|
||||
page: {
|
||||
validator: Y.Lang.isNumber,
|
||||
value: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* The number of users displayed.
|
||||
*
|
||||
* @attribute userCount
|
||||
* @default 0
|
||||
* @type Number
|
||||
*/
|
||||
userCount: {
|
||||
value: 0,
|
||||
validator: Y.Lang.isNumber
|
||||
},
|
||||
|
||||
/**
|
||||
* The number of results per page.
|
||||
*
|
||||
* @attribute perPage
|
||||
* @default 25
|
||||
* @type Number
|
||||
*/
|
||||
perPage: {
|
||||
value: 25,
|
||||
Validator: Y.Lang.isNumber
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {
|
||||
|
||||
/**
|
||||
* List of extra classes.
|
||||
*
|
||||
* @attribute extraClasses
|
||||
* @default ['gradereport_history_usp']
|
||||
* @type Array
|
||||
*/
|
||||
extraClasses: {
|
||||
value: [
|
||||
'gradereport_history_usp'
|
||||
]
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to focus on the target that caused the Widget to be shown.
|
||||
*
|
||||
* @attribute focusOnPreviousTargetAfterHide
|
||||
* @default true
|
||||
* @type Node
|
||||
*/
|
||||
focusOnPreviousTargetAfterHide: {
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Width.
|
||||
*
|
||||
* @attribute width
|
||||
* @default '500px'
|
||||
* @type String|Number
|
||||
*/
|
||||
width: {
|
||||
value: '500px'
|
||||
},
|
||||
|
||||
/**
|
||||
* Boolean indicating whether or not the Widget is visible.
|
||||
*
|
||||
* @attribute visible
|
||||
* @default false
|
||||
* @type Boolean
|
||||
*/
|
||||
visible: {
|
||||
value: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the widget should be modal or not.
|
||||
*
|
||||
* @attribute modal
|
||||
* @type Boolean
|
||||
* @default true
|
||||
*/
|
||||
modal: {
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the widget should be draggable or not.
|
||||
*
|
||||
* @attribute draggable
|
||||
* @type Boolean
|
||||
* @default true
|
||||
*/
|
||||
draggable: {
|
||||
value: true
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) {
|
||||
return new USERSELECTOR(cfg);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {
|
||||
"requires": [
|
||||
"escape",
|
||||
"event-delegate",
|
||||
"event-key",
|
||||
"handlebars",
|
||||
"io-base",
|
||||
"json-parse",
|
||||
"moodle-core-notification-dialogue"
|
||||
]
|
||||
});
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,864 @@
|
||||
YUI.add('moodle-gradereport_history-userselector', function (Y, NAME) {
|
||||
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The User Selector for the grade history report.
|
||||
*
|
||||
* @module moodle-gradereport_history-userselector
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @main moodle-gradereport_history-userselector
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module moodle-gradereport_history-userselector
|
||||
*/
|
||||
|
||||
var COMPONENT = 'gradereport_history';
|
||||
var USP = {
|
||||
AJAXURL: 'ajaxurl',
|
||||
BASE: 'base',
|
||||
CHECKBOX_NAME_PREFIX: 'usp-u',
|
||||
COURSEID: 'courseid',
|
||||
DIALOGUE_PREFIX: 'moodle-dialogue',
|
||||
NAME: 'gradereport_history_usp',
|
||||
PAGE: 'page',
|
||||
PARAMS: 'params',
|
||||
PERPAGE: 'perPage',
|
||||
SEARCH: 'search',
|
||||
SEARCHBTN: 'searchbtn',
|
||||
SELECTEDUSERS: 'selectedUsers',
|
||||
URL: 'url',
|
||||
USERCOUNT: 'userCount'
|
||||
};
|
||||
var CSS = {
|
||||
ACCESSHIDE: 'accesshide',
|
||||
AJAXCONTENT: 'usp-ajax-content',
|
||||
CHECKBOX: 'usp-checkbox',
|
||||
CLOSE: 'close',
|
||||
CLOSEBTN: 'usp-finish',
|
||||
CONTENT: 'usp-content',
|
||||
DETAILS: 'details',
|
||||
EXTRAFIELDS: 'extrafields',
|
||||
FIRSTADDED: 'usp-first-added',
|
||||
FULLNAME: 'fullname',
|
||||
HEADER: 'usp-header',
|
||||
HIDDEN: 'hidden',
|
||||
LIGHTBOX: 'usp-loading-lightbox',
|
||||
LOADINGICON: 'loading-icon',
|
||||
MORERESULTS: 'usp-more-results',
|
||||
OPTIONS: 'options',
|
||||
PICTURE: 'usp-picture',
|
||||
RESULTSCOUNT: 'usp-results-count',
|
||||
SEARCH: 'usp-search',
|
||||
SEARCHBTN: 'usp-search-btn',
|
||||
SEARCHFIELD: 'usp-search-field',
|
||||
SEARCHRESULTS: 'usp-search-results',
|
||||
SELECTED: 'selected',
|
||||
USER: 'usp-user',
|
||||
USERS: 'usp-users',
|
||||
WRAP: 'usp-wrap'
|
||||
};
|
||||
var SELECTORS = {
|
||||
AJAXCONTENT: '.' + CSS.AJAXCONTENT,
|
||||
FINISHBTN: '.' + CSS.CLOSEBTN + ' input',
|
||||
FIRSTADDED: '.' + CSS.FIRSTADDED,
|
||||
FULLNAME: '.' + CSS.FULLNAME + ' label',
|
||||
LIGHTBOX: '.' + CSS.LIGHTBOX,
|
||||
MORERESULTS: '.' + CSS.MORERESULTS,
|
||||
OPTIONS: '.' + CSS.OPTIONS,
|
||||
PICTURE: '.' + CSS.USER + ' .userpicture',
|
||||
RESULTSCOUNT: '.' + CSS.RESULTSCOUNT,
|
||||
RESULTSUSERS: '.' + CSS.SEARCHRESULTS + ' .' + CSS.USERS,
|
||||
SEARCHBTN: '.' + CSS.SEARCHBTN,
|
||||
SEARCHFIELD: '.' + CSS.SEARCHFIELD,
|
||||
SELECTEDNAMES: '.felement .selectednames',
|
||||
TRIGGER: '.gradereport_history_plugin input.selectortrigger',
|
||||
USER: '.' + CSS.USER,
|
||||
USERFULLNAMES: 'input[name="userfullnames"]',
|
||||
USERIDS: 'input[name="userids"]',
|
||||
USERSELECT: '.' + CSS.CHECKBOX + ' input[type=checkbox]'
|
||||
};
|
||||
|
||||
/**
|
||||
* User Selector.
|
||||
*
|
||||
* @namespace M.gradereport_history
|
||||
* @class UserSelector
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
var USERSELECTOR = function() {
|
||||
USERSELECTOR.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.core.dialogue, {
|
||||
|
||||
/**
|
||||
* Whether or not this is the first time the user displays the dialogue within that request.
|
||||
*
|
||||
* @property _firstDisplay
|
||||
* @type Boolean
|
||||
* @private
|
||||
*/
|
||||
_firstDisplay: true,
|
||||
|
||||
/**
|
||||
* The list of all the users selected while the dialogue is open.
|
||||
*
|
||||
* @type Object
|
||||
* @property _usersBufferList
|
||||
* @private
|
||||
*/
|
||||
_usersBufferList: null,
|
||||
|
||||
/**
|
||||
* The Node on which the focus is set.
|
||||
*
|
||||
* @property _userTabFocus
|
||||
* @type Node
|
||||
* @private
|
||||
*/
|
||||
_userTabFocus: null,
|
||||
|
||||
/**
|
||||
* Compiled template function for a user node.
|
||||
*
|
||||
* @property _userTemplate
|
||||
* @type Function
|
||||
* @private
|
||||
*/
|
||||
_userTemplate: null,
|
||||
|
||||
initializer: function() {
|
||||
var bb = this.get('boundingBox'),
|
||||
content,
|
||||
params,
|
||||
tpl;
|
||||
|
||||
tpl = Y.Handlebars.compile(
|
||||
'<div class="{{CSS.WRAP}}">' +
|
||||
'<div class="{{CSS.HEADER}}">' +
|
||||
'<div class="{{CSS.SEARCH}}" role="search">' +
|
||||
'<form>' +
|
||||
'<input type="text" class="{{CSS.SEARCHFIELD}}" ' +
|
||||
'aria-label="{{get_string "search" "moodle"}}" value="" />' +
|
||||
'<input type="submit" class="{{CSS.SEARCHBTN}}"' +
|
||||
'value="{{get_string "search" "moodle"}}">' +
|
||||
'</form>' +
|
||||
'<div aria-live="polite" class="{{CSS.RESULTSCOUNT}}">{{get_string "loading" "admin"}}</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.CONTENT}}">' +
|
||||
'<form>' +
|
||||
'<div class="{{CSS.AJAXCONTENT}}" aria-live="polite"></div>' +
|
||||
'<div class="{{CSS.LIGHTBOX}} {{CSS.HIDDEN}}">' +
|
||||
'<img class="{{CSS.LOADINGICON}}" alt="{{get_string "loading" "admin"}}"' +
|
||||
'src="{{{loadingIcon}}}">' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.CLOSEBTN}}">' +
|
||||
'<input type="submit" value="{{get_string "finishselectingusers" COMPONENT}}">' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'</div>' +
|
||||
'</div>');
|
||||
|
||||
content = Y.Node.create(
|
||||
tpl({
|
||||
COMPONENT: COMPONENT,
|
||||
CSS: CSS,
|
||||
loadingIcon: M.util.image_url('i/loading', 'moodle')
|
||||
})
|
||||
);
|
||||
|
||||
// Set the title and content.
|
||||
this.getStdModNode(Y.WidgetStdMod.HEADER).prepend(Y.Node.create('<h1>' + this.get('title') + '</h1>'));
|
||||
this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
|
||||
|
||||
// Use standard dialogue class name. This removes the default styling of the footer.
|
||||
this.get('boundingBox').one('.moodle-dialogue-wrap').addClass('moodle-dialogue-content');
|
||||
|
||||
// Add the event on the button that opens the dialogue.
|
||||
Y.one(SELECTORS.TRIGGER).on('click', this.show, this);
|
||||
|
||||
// The button to finalize the selection.
|
||||
bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this);
|
||||
|
||||
// Delegate the keyboard navigation in the users list.
|
||||
bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this);
|
||||
|
||||
// Delegate the action to select a user.
|
||||
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this);
|
||||
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this);
|
||||
|
||||
params = this.get(USP.PARAMS);
|
||||
params.id = this.get(USP.COURSEID);
|
||||
this.set(USP.PARAMS, params);
|
||||
|
||||
bb.one(SELECTORS.SEARCHBTN).on('click', this.search, this, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the dialogue.
|
||||
*
|
||||
* @method show
|
||||
*/
|
||||
show: function(e) {
|
||||
var bb;
|
||||
this._usersBufferList = Y.clone(this.get(USP.SELECTEDUSERS));
|
||||
if (this._firstDisplay) {
|
||||
// Load the default list of users when the dialogue is loaded for the first time.
|
||||
this._firstDisplay = false;
|
||||
this.search(e, false);
|
||||
} else {
|
||||
// Leave the content as is, but reset the selection.
|
||||
bb = this.get('boundingBox');
|
||||
|
||||
// Remove all the selected users.
|
||||
bb.all(SELECTORS.USER).each(function(node) {
|
||||
this.markUserNode(node, false);
|
||||
}, this);
|
||||
|
||||
// Select the users.
|
||||
Y.Object.each(this._usersBufferList, function(v, k) {
|
||||
var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]');
|
||||
if (user) {
|
||||
this.markUserNode(user, true);
|
||||
}
|
||||
}, this);
|
||||
|
||||
// Reset the tab focus.
|
||||
this.setUserTabFocus(bb.one(SELECTORS.USER));
|
||||
}
|
||||
return Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Search for users.
|
||||
*
|
||||
* @method search
|
||||
* @param {EventFacade} e The event.
|
||||
* @param {Boolean} append Whether we want to append the results to the current results or not.
|
||||
*/
|
||||
search: function(e, append) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
var params;
|
||||
if (append) {
|
||||
this.set(USP.PAGE, this.get(USP.PAGE)+1);
|
||||
} else {
|
||||
this.set(USP.USERCOUNT, 0);
|
||||
this.set(USP.PAGE, 0);
|
||||
}
|
||||
params = this.get(USP.PARAMS);
|
||||
params.sesskey = M.cfg.sesskey;
|
||||
params.action = 'searchusers';
|
||||
params.search = this.get('boundingBox').one(SELECTORS.SEARCHFIELD).get('value');
|
||||
params.page = this.get(USP.PAGE);
|
||||
params.perpage = this.get(USP.PERPAGE);
|
||||
|
||||
Y.io(M.cfg.wwwroot + this.get(USP.AJAXURL), {
|
||||
method:'POST',
|
||||
data:build_querystring(params),
|
||||
on: {
|
||||
start: this.preSearch,
|
||||
complete: this.processSearchResults,
|
||||
end: this.postSearch
|
||||
},
|
||||
context:this,
|
||||
"arguments": { // Quoted because this is a reserved keyword.
|
||||
append: append
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Pre search callback.
|
||||
*
|
||||
* @method preSearch
|
||||
* @param {String} transactionId The transaction ID.
|
||||
* @param {Object} args The arguments passed from YUI.io()
|
||||
*/
|
||||
preSearch: function(unused, args) {
|
||||
var bb = this.get('boundingBox');
|
||||
|
||||
// Display the lightbox.
|
||||
bb.one(SELECTORS.LIGHTBOX).removeClass(CSS.HIDDEN);
|
||||
|
||||
// Set the number of results to 'loading...'.
|
||||
if (!args.append) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('loading', 'admin'));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Post search callback.
|
||||
*
|
||||
* @method postSearch
|
||||
* @param {String} transactionId The transaction ID.
|
||||
* @param {Object} args The arguments passed from YUI.io()
|
||||
*/
|
||||
postSearch: function(transactionId, args) {
|
||||
var bb = this.get('boundingBox'),
|
||||
firstAdded = bb.one(SELECTORS.FIRSTADDED),
|
||||
firstUser;
|
||||
|
||||
// Hide the lightbox.
|
||||
bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN);
|
||||
|
||||
if (args.append && firstAdded) {
|
||||
// Sets the focus on the newly added user if we are appending results.
|
||||
this.setUserTabFocus(firstAdded);
|
||||
firstAdded.one(SELECTORS.USERSELECT).focus();
|
||||
} else {
|
||||
// New search result, set the tab focus on the first user returned.
|
||||
firstUser = bb.one(SELECTORS.USER);
|
||||
if (firstUser) {
|
||||
this.setUserTabFocus(firstUser);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process and display the search results.
|
||||
*
|
||||
* @method processSearchResults
|
||||
* @param {String} tid The transaction ID.
|
||||
* @param {Object} outcome The response object.
|
||||
* @param {Object} args The arguments passed from YUI.io().
|
||||
*/
|
||||
processSearchResults: function(tid, outcome, args) {
|
||||
var result = false,
|
||||
error = false,
|
||||
bb = this.get('boundingBox'),
|
||||
users,
|
||||
userTemplate,
|
||||
count,
|
||||
selected,
|
||||
i,
|
||||
firstAdded = true,
|
||||
node,
|
||||
content,
|
||||
fetchmore,
|
||||
totalUsers;
|
||||
|
||||
// Decodes the result.
|
||||
try {
|
||||
result = Y.JSON.parse(outcome.responseText);
|
||||
if (!result.success || result.error) {
|
||||
error = true;
|
||||
}
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
// There was an error.
|
||||
if (error) {
|
||||
this.setContent('');
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('errajaxsearch', COMPONENT));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the div containing the users when it is a fresh search.
|
||||
if (!args.append) {
|
||||
users = Y.Node.create('<div role="listbox" aria-activedescendant="" aria-multiselectable="true" class="'+CSS.USERS+'"></div>');
|
||||
} else {
|
||||
users = bb.one(SELECTORS.RESULTSUSERS);
|
||||
}
|
||||
|
||||
// Compile the template for each user node.
|
||||
if (!this._userTemplate) {
|
||||
this._userTemplate = Y.Handlebars.compile(
|
||||
'<div role="option" aria-selected="false" class="{{CSS.USER}} clearfix" ' +
|
||||
'data-userid="{{userId}}">' +
|
||||
'<div class="{{CSS.CHECKBOX}}">' +
|
||||
'<input name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox" tabindex="-1"' +
|
||||
'id="{{checkboxId}}" aria-describedby="{{checkboxId}} {{extraFieldsId}}"/>' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.PICTURE}}">{{{picture}}}</div>' +
|
||||
'<div class="{{CSS.DETAILS}}">' +
|
||||
'<div class="{{CSS.FULLNAME}}">' +
|
||||
'<label for="{{checkboxId}}">{{fullname}}</label>' +
|
||||
'</div>' +
|
||||
'<div id="{{extraFieldsId}}" class="{{CSS.EXTRAFIELDS}}">{{extrafields}}</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
userTemplate = this._userTemplate;
|
||||
|
||||
// Append the users one by one.
|
||||
count = this.get(USP.USERCOUNT);
|
||||
selected = '';
|
||||
for (i in result.response.users) {
|
||||
count++;
|
||||
user = result.response.users[i];
|
||||
|
||||
// If already selected.
|
||||
if (Y.Object.hasKey(this._usersBufferList, user.userid)) {
|
||||
selected = true;
|
||||
} else {
|
||||
selected = false;
|
||||
}
|
||||
|
||||
node = Y.Node.create(userTemplate({
|
||||
checkboxId: Y.guid(),
|
||||
COMPONENT: COMPONENT,
|
||||
count: count,
|
||||
CSS: CSS,
|
||||
extrafields: user.extrafields,
|
||||
extraFieldsId: Y.guid(),
|
||||
fullname: user.fullname,
|
||||
picture: user.picture,
|
||||
userId: user.userid,
|
||||
USP: USP
|
||||
}));
|
||||
|
||||
this.markUserNode(node, selected);
|
||||
|
||||
// Noting the first user that was when adding more results.
|
||||
if (args.append && firstAdded) {
|
||||
users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED);
|
||||
node.addClass(CSS.FIRSTADDED);
|
||||
firstAdded = false;
|
||||
}
|
||||
users.append(node);
|
||||
}
|
||||
this.set(USP.USERCOUNT, count);
|
||||
|
||||
// Update the count of users, and add a button to load more if need be.
|
||||
totalUsers = parseInt(result.response.totalusers, 10);
|
||||
if (!args.append) {
|
||||
if (totalUsers === 0) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('noresults', 'moodle'));
|
||||
content = '';
|
||||
} else {
|
||||
if (totalUsers === 1) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundoneuser', COMPONENT));
|
||||
} else {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundnusers', COMPONENT, totalUsers));
|
||||
}
|
||||
|
||||
content = Y.Node.create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
|
||||
.append(users);
|
||||
if (result.response.totalusers > (this.get(USP.PAGE)+1)*this.get(USP.PERPAGE)) {
|
||||
fetchmore = Y.Node.create('<div class="'+CSS.MORERESULTS+'">' +
|
||||
'<a href="#" role="button">'+M.util.get_string('loadmoreusers', COMPONENT)+'</a></div>');
|
||||
fetchmore.one('a').on('click', this.search, this, true);
|
||||
fetchmore.one('a').on('key', this.search, 'space', this, true);
|
||||
content.append(fetchmore);
|
||||
}
|
||||
}
|
||||
this.setContent(content);
|
||||
} else {
|
||||
if (totalUsers <= (this.get(USP.PAGE)+1)*this.get(USP.PERPAGE)) {
|
||||
bb.one(SELECTORS.MORERESULTS).remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user has finished selecting users.
|
||||
*
|
||||
* @method finishSelectingUsers
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
finishSelectingUsers: function(e) {
|
||||
e.preventDefault();
|
||||
this.applySelection();
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply the selection made.
|
||||
*
|
||||
* @method applySelection
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
applySelection: function() {
|
||||
var userIds = Y.Object.keys(this._usersBufferList);
|
||||
this.set(USP.SELECTEDUSERS, Y.clone(this._usersBufferList))
|
||||
.setNameDisplay();
|
||||
Y.one(SELECTORS.USERIDS).set('value', userIds.join());
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a user.
|
||||
*
|
||||
* @method SelectUser
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
selectUser: function(e) {
|
||||
var user = e.currentTarget.ancestor(SELECTORS.USER),
|
||||
checkbox = user.one(SELECTORS.USERSELECT),
|
||||
fullname = user.one(SELECTORS.FULLNAME).get('innerHTML'),
|
||||
checked = checkbox.get('checked'),
|
||||
userId = user.getData('userid');
|
||||
|
||||
if (e.currentTarget !== checkbox) {
|
||||
// We triggered the selection from another node, so we need to change the checkbox value.
|
||||
checked = !checked;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Selecting the user.
|
||||
this._usersBufferList[userId] = fullname;
|
||||
} else {
|
||||
// De-selecting the user.
|
||||
delete this._usersBufferList[userId];
|
||||
delete this._usersBufferList[parseInt(userId, 10)]; // Also remove numbered keys.
|
||||
}
|
||||
|
||||
this.markUserNode(user, checked);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark a user node as selected or not.
|
||||
*
|
||||
* This only takes care of the DOM side of things, not the internal mechanism
|
||||
* storing what users have been selected or not.
|
||||
*
|
||||
* @param {Node} node The user node.
|
||||
* @param {Boolean} selected True to mark as selected.
|
||||
* @chainable
|
||||
*/
|
||||
markUserNode: function(node, selected) {
|
||||
if (selected) {
|
||||
node.addClass(CSS.SELECTED)
|
||||
.set('aria-selected', true)
|
||||
.one(SELECTORS.USERSELECT)
|
||||
.set('checked', true);
|
||||
} else {
|
||||
node.removeClass(CSS.SELECTED)
|
||||
.set('aria-selected', false)
|
||||
.one(SELECTORS.USERSELECT)
|
||||
.set('checked', false);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the content of the dialogue.
|
||||
*
|
||||
* @method setContent
|
||||
* @param {String} content The content.
|
||||
* @chainable
|
||||
*/
|
||||
setContent: function(content) {
|
||||
this.get('boundingBox').one(SELECTORS.AJAXCONTENT).setHTML(content);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the names of the selected users in the form.
|
||||
*
|
||||
* @method setNameDisplay
|
||||
*/
|
||||
setNameDisplay: function() {
|
||||
var namelist = Y.Object.values(this.get(USP.SELECTEDUSERS));
|
||||
Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', '));
|
||||
Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join());
|
||||
},
|
||||
|
||||
/**
|
||||
* User keyboard navigation.
|
||||
*
|
||||
* @method userKeyboardNavigation
|
||||
*/
|
||||
userKeyboardNavigation: function(e) {
|
||||
var bb = this.get('boundingBox'),
|
||||
users = bb.all(SELECTORS.USER),
|
||||
direction = 1,
|
||||
user,
|
||||
current = e.target.ancestor(SELECTORS.USER, true);
|
||||
|
||||
if (e.keyCode === 38) {
|
||||
direction = -1;
|
||||
}
|
||||
|
||||
user = this.findFocusableUser(users, current, direction);
|
||||
if (user) {
|
||||
e.preventDefault();
|
||||
user.one(SELECTORS.USERSELECT).focus();
|
||||
this.setUserTabFocus(user);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the next or previous focusable node.
|
||||
*
|
||||
* @param {NodeList} users The list of users.
|
||||
* @param {Node} user The user to start with.
|
||||
* @param {Number} direction The direction in which to go.
|
||||
* @return {Node|null} A user node, or null if not found.
|
||||
* @method findFocusableUser
|
||||
*/
|
||||
findFocusableUser: function(users, user, direction) {
|
||||
var index = users.indexOf(user);
|
||||
|
||||
if (users.size() < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
return users.item(0);
|
||||
}
|
||||
|
||||
index += direction;
|
||||
|
||||
// Wrap the navigation when reaching the top of the bottom.
|
||||
if (index < 0) {
|
||||
index = users.size() - 1;
|
||||
} else if (index >= users.size()) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
return users.item(index);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the user tab focus.
|
||||
*
|
||||
* @param {Node} user The user node.
|
||||
* @method setUserTabFocus
|
||||
*/
|
||||
setUserTabFocus: function(user) {
|
||||
if (this._userTabFocus) {
|
||||
this._userTabFocus.setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
this._userTabFocus = user.one(SELECTORS.USERSELECT);
|
||||
this._userTabFocus.setAttribute('tabindex', '0');
|
||||
|
||||
this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
|
||||
}
|
||||
|
||||
}, {
|
||||
NAME: USP.NAME,
|
||||
CSS_PREFIX: USP.CSS_PREFIX,
|
||||
ATTRS: {
|
||||
|
||||
/**
|
||||
* The header.
|
||||
*
|
||||
* @attribute title
|
||||
* @default selectusers language string.
|
||||
* @type String
|
||||
*/
|
||||
title: {
|
||||
validator: Y.Lang.isString,
|
||||
valueFn: function() {
|
||||
return M.util.get_string('selectusers', COMPONENT);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The current page URL.
|
||||
*
|
||||
* @attribute url
|
||||
* @default null
|
||||
* @type String
|
||||
*/
|
||||
url: {
|
||||
validator: Y.Lang.isString,
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* The URL to the Ajax file.
|
||||
*
|
||||
* @attribute ajaxurl
|
||||
* @default null
|
||||
* @type String
|
||||
*/
|
||||
ajaxurl: {
|
||||
validator: Y.Lang.isString,
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* The names of the selected users.
|
||||
*
|
||||
* The keys are the user IDs, the values are their fullname.
|
||||
*
|
||||
* @attribute selectedUsers
|
||||
* @default null
|
||||
* @type Object
|
||||
*/
|
||||
selectedUsers: {
|
||||
validator: Y.Lang.isObject,
|
||||
value: null,
|
||||
getter: function(v) {
|
||||
if (v === null) {
|
||||
return {};
|
||||
}
|
||||
return v;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The course ID.
|
||||
*
|
||||
* @attribute courseid
|
||||
* @default null
|
||||
* @type Number
|
||||
*/
|
||||
courseid: {
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* Array of parameters.
|
||||
*
|
||||
* @attribute params
|
||||
* @default []
|
||||
* @type Array
|
||||
*/
|
||||
params: {
|
||||
validator: Y.Lang.isArray,
|
||||
value: []
|
||||
},
|
||||
|
||||
/**
|
||||
* The page we are on.
|
||||
*
|
||||
* @attribute page
|
||||
* @default 0
|
||||
* @type Number
|
||||
*/
|
||||
page: {
|
||||
validator: Y.Lang.isNumber,
|
||||
value: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* The number of users displayed.
|
||||
*
|
||||
* @attribute userCount
|
||||
* @default 0
|
||||
* @type Number
|
||||
*/
|
||||
userCount: {
|
||||
value: 0,
|
||||
validator: Y.Lang.isNumber
|
||||
},
|
||||
|
||||
/**
|
||||
* The number of results per page.
|
||||
*
|
||||
* @attribute perPage
|
||||
* @default 25
|
||||
* @type Number
|
||||
*/
|
||||
perPage: {
|
||||
value: 25,
|
||||
Validator: Y.Lang.isNumber
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {
|
||||
|
||||
/**
|
||||
* List of extra classes.
|
||||
*
|
||||
* @attribute extraClasses
|
||||
* @default ['gradereport_history_usp']
|
||||
* @type Array
|
||||
*/
|
||||
extraClasses: {
|
||||
value: [
|
||||
'gradereport_history_usp'
|
||||
]
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to focus on the target that caused the Widget to be shown.
|
||||
*
|
||||
* @attribute focusOnPreviousTargetAfterHide
|
||||
* @default true
|
||||
* @type Node
|
||||
*/
|
||||
focusOnPreviousTargetAfterHide: {
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Width.
|
||||
*
|
||||
* @attribute width
|
||||
* @default '500px'
|
||||
* @type String|Number
|
||||
*/
|
||||
width: {
|
||||
value: '500px'
|
||||
},
|
||||
|
||||
/**
|
||||
* Boolean indicating whether or not the Widget is visible.
|
||||
*
|
||||
* @attribute visible
|
||||
* @default false
|
||||
* @type Boolean
|
||||
*/
|
||||
visible: {
|
||||
value: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the widget should be modal or not.
|
||||
*
|
||||
* @attribute modal
|
||||
* @type Boolean
|
||||
* @default true
|
||||
*/
|
||||
modal: {
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the widget should be draggable or not.
|
||||
*
|
||||
* @attribute draggable
|
||||
* @type Boolean
|
||||
* @default true
|
||||
*/
|
||||
draggable: {
|
||||
value: true
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) {
|
||||
return new USERSELECTOR(cfg);
|
||||
};
|
||||
|
||||
|
||||
}, '@VERSION@', {
|
||||
"requires": [
|
||||
"escape",
|
||||
"event-delegate",
|
||||
"event-key",
|
||||
"handlebars",
|
||||
"io-base",
|
||||
"json-parse",
|
||||
"moodle-core-notification-dialogue"
|
||||
]
|
||||
});
|
10
grade/report/history/yui/src/userselector/build.json
Normal file
10
grade/report/history/yui/src/userselector/build.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "moodle-gradereport_history-userselector",
|
||||
"builds": {
|
||||
"moodle-gradereport_history-userselector": {
|
||||
"jsfiles": [
|
||||
"userselector.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
851
grade/report/history/yui/src/userselector/js/userselector.js
vendored
Normal file
851
grade/report/history/yui/src/userselector/js/userselector.js
vendored
Normal file
@ -0,0 +1,851 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* The User Selector for the grade history report.
|
||||
*
|
||||
* @module moodle-gradereport_history-userselector
|
||||
* @package gradereport_history
|
||||
* @copyright 2013 NetSpot Pty Ltd (https://www.netspot.com.au)
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @main moodle-gradereport_history-userselector
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module moodle-gradereport_history-userselector
|
||||
*/
|
||||
|
||||
var COMPONENT = 'gradereport_history';
|
||||
var USP = {
|
||||
AJAXURL: 'ajaxurl',
|
||||
BASE: 'base',
|
||||
CHECKBOX_NAME_PREFIX: 'usp-u',
|
||||
COURSEID: 'courseid',
|
||||
DIALOGUE_PREFIX: 'moodle-dialogue',
|
||||
NAME: 'gradereport_history_usp',
|
||||
PAGE: 'page',
|
||||
PARAMS: 'params',
|
||||
PERPAGE: 'perPage',
|
||||
SEARCH: 'search',
|
||||
SEARCHBTN: 'searchbtn',
|
||||
SELECTEDUSERS: 'selectedUsers',
|
||||
URL: 'url',
|
||||
USERCOUNT: 'userCount'
|
||||
};
|
||||
var CSS = {
|
||||
ACCESSHIDE: 'accesshide',
|
||||
AJAXCONTENT: 'usp-ajax-content',
|
||||
CHECKBOX: 'usp-checkbox',
|
||||
CLOSE: 'close',
|
||||
CLOSEBTN: 'usp-finish',
|
||||
CONTENT: 'usp-content',
|
||||
DETAILS: 'details',
|
||||
EXTRAFIELDS: 'extrafields',
|
||||
FIRSTADDED: 'usp-first-added',
|
||||
FULLNAME: 'fullname',
|
||||
HEADER: 'usp-header',
|
||||
HIDDEN: 'hidden',
|
||||
LIGHTBOX: 'usp-loading-lightbox',
|
||||
LOADINGICON: 'loading-icon',
|
||||
MORERESULTS: 'usp-more-results',
|
||||
OPTIONS: 'options',
|
||||
PICTURE: 'usp-picture',
|
||||
RESULTSCOUNT: 'usp-results-count',
|
||||
SEARCH: 'usp-search',
|
||||
SEARCHBTN: 'usp-search-btn',
|
||||
SEARCHFIELD: 'usp-search-field',
|
||||
SEARCHRESULTS: 'usp-search-results',
|
||||
SELECTED: 'selected',
|
||||
USER: 'usp-user',
|
||||
USERS: 'usp-users',
|
||||
WRAP: 'usp-wrap'
|
||||
};
|
||||
var SELECTORS = {
|
||||
AJAXCONTENT: '.' + CSS.AJAXCONTENT,
|
||||
FINISHBTN: '.' + CSS.CLOSEBTN + ' input',
|
||||
FIRSTADDED: '.' + CSS.FIRSTADDED,
|
||||
FULLNAME: '.' + CSS.FULLNAME + ' label',
|
||||
LIGHTBOX: '.' + CSS.LIGHTBOX,
|
||||
MORERESULTS: '.' + CSS.MORERESULTS,
|
||||
OPTIONS: '.' + CSS.OPTIONS,
|
||||
PICTURE: '.' + CSS.USER + ' .userpicture',
|
||||
RESULTSCOUNT: '.' + CSS.RESULTSCOUNT,
|
||||
RESULTSUSERS: '.' + CSS.SEARCHRESULTS + ' .' + CSS.USERS,
|
||||
SEARCHBTN: '.' + CSS.SEARCHBTN,
|
||||
SEARCHFIELD: '.' + CSS.SEARCHFIELD,
|
||||
SELECTEDNAMES: '.felement .selectednames',
|
||||
TRIGGER: '.gradereport_history_plugin input.selectortrigger',
|
||||
USER: '.' + CSS.USER,
|
||||
USERFULLNAMES: 'input[name="userfullnames"]',
|
||||
USERIDS: 'input[name="userids"]',
|
||||
USERSELECT: '.' + CSS.CHECKBOX + ' input[type=checkbox]'
|
||||
};
|
||||
|
||||
/**
|
||||
* User Selector.
|
||||
*
|
||||
* @namespace M.gradereport_history
|
||||
* @class UserSelector
|
||||
* @constructor
|
||||
*/
|
||||
|
||||
var USERSELECTOR = function() {
|
||||
USERSELECTOR.superclass.constructor.apply(this, arguments);
|
||||
};
|
||||
Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.core.dialogue, {
|
||||
|
||||
/**
|
||||
* Whether or not this is the first time the user displays the dialogue within that request.
|
||||
*
|
||||
* @property _firstDisplay
|
||||
* @type Boolean
|
||||
* @private
|
||||
*/
|
||||
_firstDisplay: true,
|
||||
|
||||
/**
|
||||
* The list of all the users selected while the dialogue is open.
|
||||
*
|
||||
* @type Object
|
||||
* @property _usersBufferList
|
||||
* @private
|
||||
*/
|
||||
_usersBufferList: null,
|
||||
|
||||
/**
|
||||
* The Node on which the focus is set.
|
||||
*
|
||||
* @property _userTabFocus
|
||||
* @type Node
|
||||
* @private
|
||||
*/
|
||||
_userTabFocus: null,
|
||||
|
||||
/**
|
||||
* Compiled template function for a user node.
|
||||
*
|
||||
* @property _userTemplate
|
||||
* @type Function
|
||||
* @private
|
||||
*/
|
||||
_userTemplate: null,
|
||||
|
||||
initializer: function() {
|
||||
var bb = this.get('boundingBox'),
|
||||
content,
|
||||
params,
|
||||
tpl;
|
||||
|
||||
tpl = Y.Handlebars.compile(
|
||||
'<div class="{{CSS.WRAP}}">' +
|
||||
'<div class="{{CSS.HEADER}}">' +
|
||||
'<div class="{{CSS.SEARCH}}" role="search">' +
|
||||
'<form>' +
|
||||
'<input type="text" class="{{CSS.SEARCHFIELD}}" ' +
|
||||
'aria-label="{{get_string "search" "moodle"}}" value="" />' +
|
||||
'<input type="submit" class="{{CSS.SEARCHBTN}}"' +
|
||||
'value="{{get_string "search" "moodle"}}">' +
|
||||
'</form>' +
|
||||
'<div aria-live="polite" class="{{CSS.RESULTSCOUNT}}">{{get_string "loading" "admin"}}</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.CONTENT}}">' +
|
||||
'<form>' +
|
||||
'<div class="{{CSS.AJAXCONTENT}}" aria-live="polite"></div>' +
|
||||
'<div class="{{CSS.LIGHTBOX}} {{CSS.HIDDEN}}">' +
|
||||
'<img class="{{CSS.LOADINGICON}}" alt="{{get_string "loading" "admin"}}"' +
|
||||
'src="{{{loadingIcon}}}">' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.CLOSEBTN}}">' +
|
||||
'<input type="submit" value="{{get_string "finishselectingusers" COMPONENT}}">' +
|
||||
'</div>' +
|
||||
'</form>' +
|
||||
'</div>' +
|
||||
'</div>');
|
||||
|
||||
content = Y.Node.create(
|
||||
tpl({
|
||||
COMPONENT: COMPONENT,
|
||||
CSS: CSS,
|
||||
loadingIcon: M.util.image_url('i/loading', 'moodle')
|
||||
})
|
||||
);
|
||||
|
||||
// Set the title and content.
|
||||
this.getStdModNode(Y.WidgetStdMod.HEADER).prepend(Y.Node.create('<h1>' + this.get('title') + '</h1>'));
|
||||
this.setStdModContent(Y.WidgetStdMod.BODY, content, Y.WidgetStdMod.REPLACE);
|
||||
|
||||
// Use standard dialogue class name. This removes the default styling of the footer.
|
||||
this.get('boundingBox').one('.moodle-dialogue-wrap').addClass('moodle-dialogue-content');
|
||||
|
||||
// Add the event on the button that opens the dialogue.
|
||||
Y.one(SELECTORS.TRIGGER).on('click', this.show, this);
|
||||
|
||||
// The button to finalize the selection.
|
||||
bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this);
|
||||
|
||||
// Delegate the keyboard navigation in the users list.
|
||||
bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this);
|
||||
|
||||
// Delegate the action to select a user.
|
||||
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this);
|
||||
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this);
|
||||
|
||||
params = this.get(USP.PARAMS);
|
||||
params.id = this.get(USP.COURSEID);
|
||||
this.set(USP.PARAMS, params);
|
||||
|
||||
bb.one(SELECTORS.SEARCHBTN).on('click', this.search, this, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the dialogue.
|
||||
*
|
||||
* @method show
|
||||
*/
|
||||
show: function(e) {
|
||||
var bb;
|
||||
this._usersBufferList = Y.clone(this.get(USP.SELECTEDUSERS));
|
||||
if (this._firstDisplay) {
|
||||
// Load the default list of users when the dialogue is loaded for the first time.
|
||||
this._firstDisplay = false;
|
||||
this.search(e, false);
|
||||
} else {
|
||||
// Leave the content as is, but reset the selection.
|
||||
bb = this.get('boundingBox');
|
||||
|
||||
// Remove all the selected users.
|
||||
bb.all(SELECTORS.USER).each(function(node) {
|
||||
this.markUserNode(node, false);
|
||||
}, this);
|
||||
|
||||
// Select the users.
|
||||
Y.Object.each(this._usersBufferList, function(v, k) {
|
||||
var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]');
|
||||
if (user) {
|
||||
this.markUserNode(user, true);
|
||||
}
|
||||
}, this);
|
||||
|
||||
// Reset the tab focus.
|
||||
this.setUserTabFocus(bb.one(SELECTORS.USER));
|
||||
}
|
||||
return Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Search for users.
|
||||
*
|
||||
* @method search
|
||||
* @param {EventFacade} e The event.
|
||||
* @param {Boolean} append Whether we want to append the results to the current results or not.
|
||||
*/
|
||||
search: function(e, append) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
var params;
|
||||
if (append) {
|
||||
this.set(USP.PAGE, this.get(USP.PAGE)+1);
|
||||
} else {
|
||||
this.set(USP.USERCOUNT, 0);
|
||||
this.set(USP.PAGE, 0);
|
||||
}
|
||||
params = this.get(USP.PARAMS);
|
||||
params.sesskey = M.cfg.sesskey;
|
||||
params.action = 'searchusers';
|
||||
params.search = this.get('boundingBox').one(SELECTORS.SEARCHFIELD).get('value');
|
||||
params.page = this.get(USP.PAGE);
|
||||
params.perpage = this.get(USP.PERPAGE);
|
||||
|
||||
Y.io(M.cfg.wwwroot + this.get(USP.AJAXURL), {
|
||||
method:'POST',
|
||||
data:build_querystring(params),
|
||||
on: {
|
||||
start: this.preSearch,
|
||||
complete: this.processSearchResults,
|
||||
end: this.postSearch
|
||||
},
|
||||
context:this,
|
||||
"arguments": { // Quoted because this is a reserved keyword.
|
||||
append: append
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Pre search callback.
|
||||
*
|
||||
* @method preSearch
|
||||
* @param {String} transactionId The transaction ID.
|
||||
* @param {Object} args The arguments passed from YUI.io()
|
||||
*/
|
||||
preSearch: function(unused, args) {
|
||||
var bb = this.get('boundingBox');
|
||||
|
||||
// Display the lightbox.
|
||||
bb.one(SELECTORS.LIGHTBOX).removeClass(CSS.HIDDEN);
|
||||
|
||||
// Set the number of results to 'loading...'.
|
||||
if (!args.append) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('loading', 'admin'));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Post search callback.
|
||||
*
|
||||
* @method postSearch
|
||||
* @param {String} transactionId The transaction ID.
|
||||
* @param {Object} args The arguments passed from YUI.io()
|
||||
*/
|
||||
postSearch: function(transactionId, args) {
|
||||
var bb = this.get('boundingBox'),
|
||||
firstAdded = bb.one(SELECTORS.FIRSTADDED),
|
||||
firstUser;
|
||||
|
||||
// Hide the lightbox.
|
||||
bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN);
|
||||
|
||||
if (args.append && firstAdded) {
|
||||
// Sets the focus on the newly added user if we are appending results.
|
||||
this.setUserTabFocus(firstAdded);
|
||||
firstAdded.one(SELECTORS.USERSELECT).focus();
|
||||
} else {
|
||||
// New search result, set the tab focus on the first user returned.
|
||||
firstUser = bb.one(SELECTORS.USER);
|
||||
if (firstUser) {
|
||||
this.setUserTabFocus(firstUser);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process and display the search results.
|
||||
*
|
||||
* @method processSearchResults
|
||||
* @param {String} tid The transaction ID.
|
||||
* @param {Object} outcome The response object.
|
||||
* @param {Object} args The arguments passed from YUI.io().
|
||||
*/
|
||||
processSearchResults: function(tid, outcome, args) {
|
||||
var result = false,
|
||||
error = false,
|
||||
bb = this.get('boundingBox'),
|
||||
users,
|
||||
userTemplate,
|
||||
count,
|
||||
selected,
|
||||
i,
|
||||
firstAdded = true,
|
||||
node,
|
||||
content,
|
||||
fetchmore,
|
||||
totalUsers;
|
||||
|
||||
// Decodes the result.
|
||||
try {
|
||||
result = Y.JSON.parse(outcome.responseText);
|
||||
if (!result.success || result.error) {
|
||||
error = true;
|
||||
}
|
||||
} catch (e) {
|
||||
error = true;
|
||||
}
|
||||
|
||||
// There was an error.
|
||||
if (error) {
|
||||
this.setContent('');
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('errajaxsearch', COMPONENT));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the div containing the users when it is a fresh search.
|
||||
if (!args.append) {
|
||||
users = Y.Node.create('<div role="listbox" aria-activedescendant="" aria-multiselectable="true" class="'+CSS.USERS+'"></div>');
|
||||
} else {
|
||||
users = bb.one(SELECTORS.RESULTSUSERS);
|
||||
}
|
||||
|
||||
// Compile the template for each user node.
|
||||
if (!this._userTemplate) {
|
||||
this._userTemplate = Y.Handlebars.compile(
|
||||
'<div role="option" aria-selected="false" class="{{CSS.USER}} clearfix" ' +
|
||||
'data-userid="{{userId}}">' +
|
||||
'<div class="{{CSS.CHECKBOX}}">' +
|
||||
'<input name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox" tabindex="-1"' +
|
||||
'id="{{checkboxId}}" aria-describedby="{{checkboxId}} {{extraFieldsId}}"/>' +
|
||||
'</div>' +
|
||||
'<div class="{{CSS.PICTURE}}">{{{picture}}}</div>' +
|
||||
'<div class="{{CSS.DETAILS}}">' +
|
||||
'<div class="{{CSS.FULLNAME}}">' +
|
||||
'<label for="{{checkboxId}}">{{fullname}}</label>' +
|
||||
'</div>' +
|
||||
'<div id="{{extraFieldsId}}" class="{{CSS.EXTRAFIELDS}}">{{extrafields}}</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
userTemplate = this._userTemplate;
|
||||
|
||||
// Append the users one by one.
|
||||
count = this.get(USP.USERCOUNT);
|
||||
selected = '';
|
||||
for (i in result.response.users) {
|
||||
count++;
|
||||
user = result.response.users[i];
|
||||
|
||||
// If already selected.
|
||||
if (Y.Object.hasKey(this._usersBufferList, user.userid)) {
|
||||
selected = true;
|
||||
} else {
|
||||
selected = false;
|
||||
}
|
||||
|
||||
node = Y.Node.create(userTemplate({
|
||||
checkboxId: Y.guid(),
|
||||
COMPONENT: COMPONENT,
|
||||
count: count,
|
||||
CSS: CSS,
|
||||
extrafields: user.extrafields,
|
||||
extraFieldsId: Y.guid(),
|
||||
fullname: user.fullname,
|
||||
picture: user.picture,
|
||||
userId: user.userid,
|
||||
USP: USP
|
||||
}));
|
||||
|
||||
this.markUserNode(node, selected);
|
||||
|
||||
// Noting the first user that was when adding more results.
|
||||
if (args.append && firstAdded) {
|
||||
users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED);
|
||||
node.addClass(CSS.FIRSTADDED);
|
||||
firstAdded = false;
|
||||
}
|
||||
users.append(node);
|
||||
}
|
||||
this.set(USP.USERCOUNT, count);
|
||||
|
||||
// Update the count of users, and add a button to load more if need be.
|
||||
totalUsers = parseInt(result.response.totalusers, 10);
|
||||
if (!args.append) {
|
||||
if (totalUsers === 0) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('noresults', 'moodle'));
|
||||
content = '';
|
||||
} else {
|
||||
if (totalUsers === 1) {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundoneuser', COMPONENT));
|
||||
} else {
|
||||
bb.one(SELECTORS.RESULTSCOUNT).setHTML(M.util.get_string('foundnusers', COMPONENT, totalUsers));
|
||||
}
|
||||
|
||||
content = Y.Node.create('<div class="'+CSS.SEARCHRESULTS+'"></div>')
|
||||
.append(users);
|
||||
if (result.response.totalusers > (this.get(USP.PAGE)+1)*this.get(USP.PERPAGE)) {
|
||||
fetchmore = Y.Node.create('<div class="'+CSS.MORERESULTS+'">' +
|
||||
'<a href="#" role="button">'+M.util.get_string('loadmoreusers', COMPONENT)+'</a></div>');
|
||||
fetchmore.one('a').on('click', this.search, this, true);
|
||||
fetchmore.one('a').on('key', this.search, 'space', this, true);
|
||||
content.append(fetchmore);
|
||||
}
|
||||
}
|
||||
this.setContent(content);
|
||||
} else {
|
||||
if (totalUsers <= (this.get(USP.PAGE)+1)*this.get(USP.PERPAGE)) {
|
||||
bb.one(SELECTORS.MORERESULTS).remove();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user has finished selecting users.
|
||||
*
|
||||
* @method finishSelectingUsers
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
finishSelectingUsers: function(e) {
|
||||
e.preventDefault();
|
||||
this.applySelection();
|
||||
this.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply the selection made.
|
||||
*
|
||||
* @method applySelection
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
applySelection: function() {
|
||||
var userIds = Y.Object.keys(this._usersBufferList);
|
||||
this.set(USP.SELECTEDUSERS, Y.clone(this._usersBufferList))
|
||||
.setNameDisplay();
|
||||
Y.one(SELECTORS.USERIDS).set('value', userIds.join());
|
||||
},
|
||||
|
||||
/**
|
||||
* Select a user.
|
||||
*
|
||||
* @method SelectUser
|
||||
* @param {EventFacade} e The event.
|
||||
*/
|
||||
selectUser: function(e) {
|
||||
var user = e.currentTarget.ancestor(SELECTORS.USER),
|
||||
checkbox = user.one(SELECTORS.USERSELECT),
|
||||
fullname = user.one(SELECTORS.FULLNAME).get('innerHTML'),
|
||||
checked = checkbox.get('checked'),
|
||||
userId = user.getData('userid');
|
||||
|
||||
if (e.currentTarget !== checkbox) {
|
||||
// We triggered the selection from another node, so we need to change the checkbox value.
|
||||
checked = !checked;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
// Selecting the user.
|
||||
this._usersBufferList[userId] = fullname;
|
||||
} else {
|
||||
// De-selecting the user.
|
||||
delete this._usersBufferList[userId];
|
||||
delete this._usersBufferList[parseInt(userId, 10)]; // Also remove numbered keys.
|
||||
}
|
||||
|
||||
this.markUserNode(user, checked);
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark a user node as selected or not.
|
||||
*
|
||||
* This only takes care of the DOM side of things, not the internal mechanism
|
||||
* storing what users have been selected or not.
|
||||
*
|
||||
* @param {Node} node The user node.
|
||||
* @param {Boolean} selected True to mark as selected.
|
||||
* @chainable
|
||||
*/
|
||||
markUserNode: function(node, selected) {
|
||||
if (selected) {
|
||||
node.addClass(CSS.SELECTED)
|
||||
.set('aria-selected', true)
|
||||
.one(SELECTORS.USERSELECT)
|
||||
.set('checked', true);
|
||||
} else {
|
||||
node.removeClass(CSS.SELECTED)
|
||||
.set('aria-selected', false)
|
||||
.one(SELECTORS.USERSELECT)
|
||||
.set('checked', false);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the content of the dialogue.
|
||||
*
|
||||
* @method setContent
|
||||
* @param {String} content The content.
|
||||
* @chainable
|
||||
*/
|
||||
setContent: function(content) {
|
||||
this.get('boundingBox').one(SELECTORS.AJAXCONTENT).setHTML(content);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the names of the selected users in the form.
|
||||
*
|
||||
* @method setNameDisplay
|
||||
*/
|
||||
setNameDisplay: function() {
|
||||
var namelist = Y.Object.values(this.get(USP.SELECTEDUSERS));
|
||||
Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', '));
|
||||
Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join());
|
||||
},
|
||||
|
||||
/**
|
||||
* User keyboard navigation.
|
||||
*
|
||||
* @method userKeyboardNavigation
|
||||
*/
|
||||
userKeyboardNavigation: function(e) {
|
||||
var bb = this.get('boundingBox'),
|
||||
users = bb.all(SELECTORS.USER),
|
||||
direction = 1,
|
||||
user,
|
||||
current = e.target.ancestor(SELECTORS.USER, true);
|
||||
|
||||
if (e.keyCode === 38) {
|
||||
direction = -1;
|
||||
}
|
||||
|
||||
user = this.findFocusableUser(users, current, direction);
|
||||
if (user) {
|
||||
e.preventDefault();
|
||||
user.one(SELECTORS.USERSELECT).focus();
|
||||
this.setUserTabFocus(user);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the next or previous focusable node.
|
||||
*
|
||||
* @param {NodeList} users The list of users.
|
||||
* @param {Node} user The user to start with.
|
||||
* @param {Number} direction The direction in which to go.
|
||||
* @return {Node|null} A user node, or null if not found.
|
||||
* @method findFocusableUser
|
||||
*/
|
||||
findFocusableUser: function(users, user, direction) {
|
||||
var index = users.indexOf(user);
|
||||
|
||||
if (users.size() < 1) {
|
||||
Y.log('The users list is empty', 'debug', COMPONENT);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (index < 0) {
|
||||
Y.log('Unable to find the user in the list of users', 'debug', COMPONENT);
|
||||
return users.item(0);
|
||||
}
|
||||
|
||||
index += direction;
|
||||
|
||||
// Wrap the navigation when reaching the top of the bottom.
|
||||
if (index < 0) {
|
||||
index = users.size() - 1;
|
||||
} else if (index >= users.size()) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
return users.item(index);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the user tab focus.
|
||||
*
|
||||
* @param {Node} user The user node.
|
||||
* @method setUserTabFocus
|
||||
*/
|
||||
setUserTabFocus: function(user) {
|
||||
if (this._userTabFocus) {
|
||||
this._userTabFocus.setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
this._userTabFocus = user.one(SELECTORS.USERSELECT);
|
||||
this._userTabFocus.setAttribute('tabindex', '0');
|
||||
|
||||
this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
|
||||
}
|
||||
|
||||
}, {
|
||||
NAME: USP.NAME,
|
||||
CSS_PREFIX: USP.CSS_PREFIX,
|
||||
ATTRS: {
|
||||
|
||||
/**
|
||||
* The header.
|
||||
*
|
||||
* @attribute title
|
||||
* @default selectusers language string.
|
||||
* @type String
|
||||
*/
|
||||
title: {
|
||||
validator: Y.Lang.isString,
|
||||
valueFn: function() {
|
||||
return M.util.get_string('selectusers', COMPONENT);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The current page URL.
|
||||
*
|
||||
* @attribute url
|
||||
* @default null
|
||||
* @type String
|
||||
*/
|
||||
url: {
|
||||
validator: Y.Lang.isString,
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* The URL to the Ajax file.
|
||||
*
|
||||
* @attribute ajaxurl
|
||||
* @default null
|
||||
* @type String
|
||||
*/
|
||||
ajaxurl: {
|
||||
validator: Y.Lang.isString,
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* The names of the selected users.
|
||||
*
|
||||
* The keys are the user IDs, the values are their fullname.
|
||||
*
|
||||
* @attribute selectedUsers
|
||||
* @default null
|
||||
* @type Object
|
||||
*/
|
||||
selectedUsers: {
|
||||
validator: Y.Lang.isObject,
|
||||
value: null,
|
||||
getter: function(v) {
|
||||
if (v === null) {
|
||||
return {};
|
||||
}
|
||||
return v;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The course ID.
|
||||
*
|
||||
* @attribute courseid
|
||||
* @default null
|
||||
* @type Number
|
||||
*/
|
||||
courseid: {
|
||||
value: null
|
||||
},
|
||||
|
||||
/**
|
||||
* Array of parameters.
|
||||
*
|
||||
* @attribute params
|
||||
* @default []
|
||||
* @type Array
|
||||
*/
|
||||
params: {
|
||||
validator: Y.Lang.isArray,
|
||||
value: []
|
||||
},
|
||||
|
||||
/**
|
||||
* The page we are on.
|
||||
*
|
||||
* @attribute page
|
||||
* @default 0
|
||||
* @type Number
|
||||
*/
|
||||
page: {
|
||||
validator: Y.Lang.isNumber,
|
||||
value: 0
|
||||
},
|
||||
|
||||
/**
|
||||
* The number of users displayed.
|
||||
*
|
||||
* @attribute userCount
|
||||
* @default 0
|
||||
* @type Number
|
||||
*/
|
||||
userCount: {
|
||||
value: 0,
|
||||
validator: Y.Lang.isNumber
|
||||
},
|
||||
|
||||
/**
|
||||
* The number of results per page.
|
||||
*
|
||||
* @attribute perPage
|
||||
* @default 25
|
||||
* @type Number
|
||||
*/
|
||||
perPage: {
|
||||
value: 25,
|
||||
Validator: Y.Lang.isNumber
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {
|
||||
|
||||
/**
|
||||
* List of extra classes.
|
||||
*
|
||||
* @attribute extraClasses
|
||||
* @default ['gradereport_history_usp']
|
||||
* @type Array
|
||||
*/
|
||||
extraClasses: {
|
||||
value: [
|
||||
'gradereport_history_usp'
|
||||
]
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to focus on the target that caused the Widget to be shown.
|
||||
*
|
||||
* @attribute focusOnPreviousTargetAfterHide
|
||||
* @default true
|
||||
* @type Node
|
||||
*/
|
||||
focusOnPreviousTargetAfterHide: {
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Width.
|
||||
*
|
||||
* @attribute width
|
||||
* @default '500px'
|
||||
* @type String|Number
|
||||
*/
|
||||
width: {
|
||||
value: '500px'
|
||||
},
|
||||
|
||||
/**
|
||||
* Boolean indicating whether or not the Widget is visible.
|
||||
*
|
||||
* @attribute visible
|
||||
* @default false
|
||||
* @type Boolean
|
||||
*/
|
||||
visible: {
|
||||
value: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the widget should be modal or not.
|
||||
*
|
||||
* @attribute modal
|
||||
* @type Boolean
|
||||
* @default true
|
||||
*/
|
||||
modal: {
|
||||
value: true
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the widget should be draggable or not.
|
||||
*
|
||||
* @attribute draggable
|
||||
* @type Boolean
|
||||
* @default true
|
||||
*/
|
||||
draggable: {
|
||||
value: true
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) {
|
||||
return new USERSELECTOR(cfg);
|
||||
};
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"moodle-gradereport_history-userselector": {
|
||||
"requires": [
|
||||
"escape",
|
||||
"event-delegate",
|
||||
"event-key",
|
||||
"handlebars",
|
||||
"io-base",
|
||||
"json-parse",
|
||||
"moodle-core-notification-dialogue"
|
||||
]
|
||||
}
|
||||
}
|
@ -1034,7 +1034,7 @@ class core_plugin_manager {
|
||||
),
|
||||
|
||||
'gradereport' => array(
|
||||
'grader', 'outcomes', 'overview', 'user'
|
||||
'grader', 'history', 'outcomes', 'overview', 'user'
|
||||
),
|
||||
|
||||
'gradingform' => array(
|
||||
|
Loading…
x
Reference in New Issue
Block a user