Merge branch 'MDL-46191-master' of git://github.com/FMCorz/moodle

This commit is contained in:
Marina Glancy 2014-08-25 15:43:29 +08:00
commit cf2d4159b7
21 changed files with 4350 additions and 1 deletions

View 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');
}
}

View 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'));
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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');
}
}

View 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'
)
);

View 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();

View 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';

View 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
));
}

View 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;
}

View 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);
}
}
}

View 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();

View 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';

View File

@ -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

View File

@ -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"
]
});

View File

@ -0,0 +1,10 @@
{
"name": "moodle-gradereport_history-userselector",
"builds": {
"moodle-gradereport_history-userselector": {
"jsfiles": [
"userselector.js"
]
}
}
}

View 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);
};

View File

@ -0,0 +1,13 @@
{
"moodle-gradereport_history-userselector": {
"requires": [
"escape",
"event-delegate",
"event-key",
"handlebars",
"io-base",
"json-parse",
"moodle-core-notification-dialogue"
]
}
}

View File

@ -1034,7 +1034,7 @@ class core_plugin_manager {
),
'gradereport' => array(
'grader', 'outcomes', 'overview', 'user'
'grader', 'history', 'outcomes', 'overview', 'user'
),
'gradingform' => array(