first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -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');
}
}
@@ -0,0 +1,83 @@
<?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'];
$userfullnames = $this->_customdata['userfullnames'];
$names = \html_writer::span($userfullnames, '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('dateto', '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'));
}
}
+226
View File
@@ -0,0 +1,226 @@
<?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) {
global $DB, $USER;
$userfieldsapi = \core_user\fields::for_identity($context)->with_userpic()->including('username');
$userfieldssql = $userfieldsapi->get_sql('u', true, '', '', false);
// Fields we need from the user table.
$extrafields = [];
foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
$extrafields[$field] = $userfieldssql->mappings[$field];
}
$params = array();
if (!empty($search)) {
list($filtersql, $params) = users_search_sql($search, 'u', USER_SEARCH_CONTAINS, $extrafields);
$filtersql .= ' AND ';
} else {
$filtersql = '';
}
$userfieldjoinssql = $userfieldssql->joins;
if ($count) {
$select = "SELECT COUNT(DISTINCT u.id) ";
$orderby = "";
} else {
$select = "SELECT DISTINCT $userfieldssql->selects ";
$orderby = " ORDER BY u.lastname ASC, u.firstname ASC";
}
$groupjoinsql = '';
$groupwheresql = '';
$courseid = $context->instanceid;
$groupmode = groups_get_course_groupmode(get_course($courseid));
// We're only interested in separate groups mode because it's the only group mode that requires the user to be a member of
// specific group(s), except when they have the 'moodle/site:accessallgroups' capability.
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) {
// Fetch the groups that the user can see.
$groups = groups_get_all_groups($courseid, $USER->id, 0, 'g.id');
// Add join condition to include users that only belong to the same group as the user.
list($insql, $inparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED, 'gid', true, 0);
$groupjoinsql = " JOIN {groups_members} gm ON gm.userid = u.id ";
$groupwheresql = " AND gm.groupid $insql ";
$params = array_merge($params, $inparams);
}
$sql = "$select
FROM {user} u
JOIN {grade_grades_history} ggh ON u.id = ggh.userid
JOIN {grade_items} gi ON gi.id = ggh.itemid
$userfieldjoinssql
$groupjoinsql
WHERE $filtersql gi.courseid = :courseid $groupwheresql";
$sql .= $orderby;
$params['courseid'] = $courseid;
$params = array_merge($userfieldssql->params, $params);
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, $USER;
$groupjoinsql = $groupwheresql = '';
$inparams = [];
$groupmode = groups_get_course_groupmode(get_course($courseid));
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', \context_course::instance($courseid))) {
// Fetch the groups that the user can see.
$groups = groups_get_all_groups($courseid, $USER->id, 0, 'g.id');
// Add join condition to include users that only belong to the same group as the user.
list($insql, $inparams) = $DB->get_in_or_equal(array_keys($groups), SQL_PARAMS_NAMED, 'gid', true, 0);
$groupjoinsql = " JOIN {groups_members} gm ON gm.userid = u.id ";
$groupwheresql = " AND gm.groupid $insql ";
}
$userfieldsapi = \core_user\fields::for_name();
$ufields = $userfieldsapi->get_sql('u', false, '', '', false)->selects;
$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
$groupjoinsql
WHERE gi.courseid = :courseid $groupwheresql
GROUP BY u.id, $ufields
ORDER BY u.lastname ASC, u.firstname ASC";
$graders = $DB->get_records_sql($sql, array('courseid' => $courseid) + $inparams);
$return = array(0 => get_string('allgraders', 'gradereport_history'));
foreach ($graders as $grader) {
$return[$grader->id] = fullname($grader);
}
return $return;
}
}
@@ -0,0 +1,70 @@
<?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) {
$data = $button->export_for_template($this);
return $this->render_from_template('gradereport_history/user_button', $data);
}
/**
* 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;
}
}
@@ -0,0 +1,566 @@
<?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');
require_once($CFG->dirroot . '/user/lib.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 \stdClass[] List of users included in the report (if userids are specified as filters)
*/
protected $users = [];
/**
* @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;
/**
* @var int The default number of decimal points to use in this course
* when a grade item does not itself define the number of decimal points.
*/
protected $defaultdecimalpoints;
/**
* 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) {
global $CFG;
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->currpage = $page;
$this->gradeitems = \grade_item::fetch_all(array('courseid' => $this->courseid));
$this->cms = get_fast_modinfo($this->courseid);
$this->useridfield = 'userid';
$this->defaultdecimalpoints = grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
// Define columns in the table.
$this->define_table_columns();
// Define filters.
$this->define_table_filters((object) $filters);
// Define configs.
$this->define_table_configs($url);
// Set download status.
$this->is_downloading($download, get_string('exportfilename', 'gradereport_history'));
}
/**
* 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');
}
/**
* Define table filters
*
* @param \stdClass $filters
*/
protected function define_table_filters(\stdClass $filters): void {
global $DB;
$this->filters = $filters;
if (!empty($this->filters->userids)) {
$course = get_course($this->courseid);
// Retrieve userids that are part of the filters object, and ensure user can access each of them.
[$userselect, $userparams] = $DB->get_in_or_equal(explode(',', $this->filters->userids), SQL_PARAMS_NAMED);
[$usersort] = users_order_by_sql();
$this->users = array_filter(
$DB->get_records_select('user', "id {$userselect}", $userparams, $usersort),
static function(\stdClass $user) use ($course): bool {
return user_can_view_profile($user, $course);
}
);
// Reset userids to the filtered array of users.
$this->filters->userids = implode(',', array_keys($this->users));
}
}
/**
* Setup the headers for the html table.
*/
protected function define_table_columns() {
$extrafields = \core_user\fields::get_identity_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] = \core_user\fields::get_display_name($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 the final grade.
*
* @param \stdClass $history an entry of history record.
*
* @return string HTML to display
*/
public function col_finalgrade(\stdClass $history) {
if (!empty($this->gradeitems[$history->itemid])) {
$decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
} else {
$decimalpoints = $this->defaultdecimalpoints;
}
return format_float($history->finalgrade, $decimalpoints);
}
/**
* Method to display the previous grade.
*
* @param \stdClass $history an entry of history record.
*
* @return string HTML to display
*/
public function col_prevgrade(\stdClass $history) {
if (!empty($this->gradeitems[$history->itemid])) {
$decimalpoints = $this->gradeitems[$history->itemid]->get_decimals();
} else {
$decimalpoints = $this->defaultdecimalpoints;
}
return format_float($history->prevgrade, $decimalpoints);
}
/**
* 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) {
if (empty($history->usermodified)) {
// Not every row has a valid usermodified.
return '';
}
$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 {
// We need the activity context, not the course context.
$gradeitem = $this->gradeitems[$history->itemid];
$context = $gradeitem->get_context();
$feedback = file_rewrite_pluginfile_urls(
$history->feedback,
'pluginfile.php',
$context->id,
GRADE_FILE_COMPONENT,
GRADE_HISTORY_FEEDBACK_FILEAREA,
$history->id
);
return format_text($feedback, $history->feedbackformat, array('context' => $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, $USER;
$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);
}
// If the course is separate group mode and the current user is not allowed to see all groups make sure
// that we display only users from the same groups as current user.
$groupmode = get_course($coursecontext->instanceid)->groupmode;
if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $coursecontext)) {
$groupids = array_column(groups_get_all_groups($coursecontext->instanceid, $USER->id, 0, 'g.id'), 'id');
list($gsql, $gparams) = $DB->get_in_or_equal($groupids, SQL_PARAMS_NAMED, 'gmuparam', true, 0);
$filter .= " AND EXISTS (SELECT 1 FROM {groups_members} gmu WHERE gmu.userid=ggh.userid AND gmu.groupid $gsql)";
$params += $gparams;
}
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, ';
$userfieldsapi = \core_user\fields::for_identity($this->context);
$userfieldssql = $userfieldsapi->get_sql('u', true, '', '', true);
$userfieldsselects = '';
$userfieldsjoins = '';
$userfieldsparams = [];
if (!$count) {
$userfieldsselects = $userfieldssql->selects;
$userfieldsjoins = $userfieldssql->joins;
$userfieldsparams = $userfieldssql->params;
}
// Add extra user fields that we need for the graded user.
$extrafields = [];
foreach ($userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]) as $field) {
$extrafields[$field] = $userfieldssql->mappings[$field];
}
$userfieldsapi = \core_user\fields::for_name();
$fields .= $userfieldsapi->get_sql('u', false, '', '', false)->selects . ', ';
$groupby = $fields;
// Add extra user fields that we need for the grader user.
$fields .= $userfieldsapi->get_sql('ug', false, 'grader', '', false)->selects;
$groupby .= $userfieldsapi->get_sql('ug', false, '', '', false)->selects;
// 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 $userfieldsselects
FROM {grade_grades_history} ggh
JOIN {grade_items} gi ON gi.id = ggh.itemid
JOIN {user} u ON u.id = ggh.userid
$userfieldsjoins
LEFT JOIN {user} ug ON ug.id = ggh.usermodified
WHERE $where";
$params = array_merge($userfieldsparams, $params);
// 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 && $sqlsort = $this->get_sql_sort()) {
$sql .= " ORDER BY " . $sqlsort;
}
return array($sql, $params);
}
/**
* Get the SQL fragment to sort by.
*
* This is overridden to sort by timemodified and ID by default. Many items happen at the same time
* and a second sorting by ID is valuable to distinguish the order in which the history happened.
*
* @return string SQL fragment.
*/
public function get_sql_sort() {
$columns = $this->get_sort_columns();
if (count($columns) == 1 && isset($columns['timemodified']) && $columns['timemodified'] == SORT_DESC) {
// Add the 'id' column when we are using the default sorting.
$columns['id'] = SORT_DESC;
return self::construct_order_by($columns);
}
return parent::get_sql_sort();
}
/**
* 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);
if ($this->is_downloading()) {
$histories = $DB->get_records_sql($sql, $params);
} else {
$histories = $DB->get_records_sql($sql, $params, $this->pagesize * $this->currpage, $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 \stdClass[] List of user objects
*/
public function get_selected_users(): array {
return $this->users;
}
}
@@ -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');
}
}
@@ -0,0 +1,46 @@
<?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/>.
/**
* Privacy Subsystem implementation for gradereport_history.
*
* @package gradereport_history
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace gradereport_history\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for gradereport_history implementing null_provider.
*
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason(): string {
return 'privacy:metadata';
}
}
+41
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'
)
);
+45
View File
@@ -0,0 +1,45 @@
<?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 overview report upgrade steps.
*
* @package gradereport_history
* @copyright 2020 Michael Hawkins <michaelh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Function to upgrade grade history report.
*
* @param int $oldversion the version we are upgrading from
* @return bool result
*/
function xmldb_gradereport_history_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
+130
View File
@@ -0,0 +1,130 @@
<?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.
$showreport = optional_param('showreport', 0, PARAM_INT);
$PAGE->set_pagelayout('report');
$url = new moodle_url('/grade/report/history/index.php', array('id' => $courseid, 'showreport' => 1));
$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 courseid = :courseid AND " . $DB->sql_isnotempty('grade_items', 'itemname', true, true);
$itemids = $DB->get_records_select_menu('grade_items', $select, array('courseid' => $course->id), 'itemname ASC', 'id, itemname');
foreach ($itemids as $itemid => $itemname) {
$itemids[$itemid] = format_string($itemname, false, array("context" => $context));
}
$itemids = array(0 => get_string('allgradeitems', 'gradereport_history')) + $itemids;
$output = $PAGE->get_renderer('gradereport_history');
$graders = \gradereport_history\helper::get_graders($course->id);
$params = ['course' => $course, 'itemids' => $itemids, 'graders' => $graders,
'userbutton' => null, 'userfullnames' => ''];
$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);
}
$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 = ['course' => $course, 'itemids' => $itemids, 'graders' => $graders,
'userbutton' => $userbutton, 'userfullnames' => $userfullnames];
$mform = new \gradereport_history\filter_form(null, $params);
$mform->set_data($filters);
if ($table->is_downloading()) {
// Download file and exit.
\core\session\manager::write_close();
echo $output->render($table);
die();
}
// Print header.
$actionbar = new \core_grades\output\general_action_bar($context,
new moodle_url('/grade/report/history/index.php', ['id' => $courseid]), 'report', 'history');
print_grade_page_head($COURSE->id, 'report', 'history', false, false, false, true, null, null,
null, $actionbar);
$mform->display();
if ($showreport) {
// Only display report after form has been submitted.
echo $output->render($table);
$event = \gradereport_history\event\grade_report_viewed::create(
array(
'context' => $context,
'courseid' => $courseid
)
);
$event->trigger();
}
echo $OUTPUT->footer();
@@ -0,0 +1,59 @@
<?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['dateto'] = 'Date to';
$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['exportfilename'] = 'grade_history';
$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 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['privacy:metadata'] = 'The Grade history report only shows data stored in other locations.';
$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';
+37
View File
@@ -0,0 +1,37 @@
<?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,
PARAM_INT
));
}
+106
View File
@@ -0,0 +1,106 @@
/* 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;
}
@@ -0,0 +1,63 @@
{{!
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/>.
}}
{{!
@template gradereport_history/user_button
Template which defines a forum post for sending in a single-post HTML email.
Classes required for JS:
* none
Data attributes required for JS:
* none
Example context (json):
{
"name": "showreport",
"label": "Select users",
"value": "1",
"classes": "singlebutton selectusersbutton gradereport_history_plugin",
"method": "get",
"url": "/grade/report/history/index.php",
"formid": "selectusersbutton{generated}",
"params": "true",
"id": "single_button{generated}",
"tooltip": "Select users",
"disabled": ""
}
}}
<div class="{{classes}}">
<div method="{{method}}" action="{{url}}" id="{{formid}}">
{{#params}}
<input type="hidden" name="{{name}}" value="{{value}}">
{{/params}}
<input type="button" class="btn btn-secondary selectortrigger"
id="{{id}}"
title="{{tooltip}}"
value="{{label}}"
{{#disabled}}disabled{{/disabled}}>
</div>
</div>
{{#hasactions}}
{{#js}}
require(['core/yui'], function(Y) {
{{#actions}}
Y.on('{{event}}', {{{jsfunction}}}, '#{{id}}', null{{#jsfunctionargs}}, {{{jsfunctionargs}}}{{/jsfunctionargs}});
{{/actions}}
});
{{/js}}
{{/hasactions}}
@@ -0,0 +1,118 @@
@gradereport @gradereport_history @_bug_phantomjs
Feature: A teacher checks the grade history report in a course
In order to check the history of the grades
As a teacher
I need to check that the history report is correctly displaying changes
@javascript
Scenario: Check the history report displays results correctly
Given the following "courses" exist:
| fullname | shortname | category | groupmode |
| Course 1 | C1 | 0 | 1 |
And the following "custom profile fields" exist:
| datatype | shortname | name |
| text | food | Favourite food |
And the following "users" exist:
| username | firstname | lastname | email | profile_field_food |
| teacher1 | Teacher | 1 | teacher1@example.com | |
| teacher2 | Teacher | 2 | teacher2@example.com | |
| student1 | Student | 1 | student1@example.com | apple |
| student2 | Student | 2 | student2@example.com | orange |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
And the following "activities" exist:
| activity | course | name |
| assign | C1 | The greatest assignment ever |
| assign | C1 | Rewarding assignment |
Given the following config values are set as admin:
| showuseridentity | email,profile_field_food |
And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher1"
And I should see "apple" in the "student1" "table_row"
And I should see "orange" in the "student2" "table_row"
And I turn editing mode on
And I give the grade "50.00" to the user "Student 1" for the grade item "The greatest assignment ever"
And I give the grade "60.00" to the user "Student 1" for the grade item "Rewarding assignment"
And I give the grade "50.00" to the user "Student 2" for the grade item "The greatest assignment ever"
And I give the grade "60.00" to the user "Student 2" for the grade item "Rewarding assignment"
And I press "Save changes"
And I am on the "Course 1" "grades > Grader report > View" page logged in as "teacher2"
And I should see "apple" in the "student1" "table_row"
And I should see "orange" in the "student2" "table_row"
And I turn editing mode on
And I give the grade "70.00" to the user "Student 1" for the grade item "The greatest assignment ever"
And I give the grade "80.00" to the user "Student 1" for the grade item "Rewarding assignment"
And I give the grade "70.00" to the user "Student 2" for the grade item "The greatest assignment ever"
And I give the grade "80.00" to the user "Student 2" for the grade item "Rewarding assignment"
And I press "Save changes"
And I navigate to "View > Grade history" in the course gradebook
When I press "Submit"
Then the following should exist in the "gradereport_history" table:
| First name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader |
| Student 1 | student1@example.com | apple | The greatest assignment ever | | 50.00 | Teacher 1 |
| Student 1 | student1@example.com | apple | Rewarding assignment | | 60.00 | Teacher 1 |
| Student 2 | student2@example.com | orange | The greatest assignment ever | | 50.00 | Teacher 1 |
| Student 2 | student2@example.com | orange | Rewarding assignment | | 60.00 | Teacher 1 |
| Student 1 | student1@example.com | apple | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 |
| Student 1 | student1@example.com | apple | Rewarding assignment | 60.00 | 80.00 | Teacher 2 |
| Student 2 | student2@example.com | orange | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 |
| Student 2 | student2@example.com | orange | Rewarding assignment | 60.00 | 80.00 | Teacher 2 |
# Test filtering by student - display of several users.
And I press "Select users"
And I click on "Student 1" "checkbox"
And I click on "Student 2" "checkbox"
And I press "Finish selecting users"
And I should see "Student 1, Student 2"
And I press "Submit"
And I should see "Student 1, Student 2"
# Test filtering by student.
And I press "Select users"
And I set the field with xpath "//form/input[@class='usp-search-field']" to "Student 2"
And I click on "Search" "button" in the "Select users" "dialogue"
And I should see "Student 2" in the "Select users" "dialogue"
And I should not see "Student 1" in the "Select users" "dialogue"
# Deselect.
And I click on "Student 2" "checkbox"
And I press "Finish selecting users"
And I press "Submit"
And the following should exist in the "gradereport_history" table:
| First name | Grade item | Original grade | Revised grade | Grader |
| Student 1 | The greatest assignment ever | | 50.00 | Teacher 1 |
| Student 1 | Rewarding assignment | | 60.00 | Teacher 1 |
| Student 1 | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 |
| Student 1 | Rewarding assignment | 60.00 | 80.00 | Teacher 2 |
# Test for seeing custom fields contents in the rows.
And I should see "apple" in the "student1" "table_row"
And I should not see "orange"
And the following should not exist in the "gradereport_history" table:
| Student 2 | The greatest assignment ever | | 50.00 | Teacher 1 |
| Student 2 | Rewarding assignment | | 60.00 | Teacher 1 |
| Student 2 | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 |
| Student 2 | Rewarding assignment | 60.00 | 80.00 | Teacher 2 |
# Test filtering by assignment.
And I set the field "Grade item" to "The greatest assignment ever"
And I press "Submit"
And the following should exist in the "gradereport_history" table:
| First name | Grade item | Original grade | Revised grade | Grader |
| Student 1 | The greatest assignment ever | | 50.00 | Teacher 1 |
| Student 1 | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 |
And the following should not exist in the "gradereport_history" table:
| Student 1 | Rewarding assignment | | 60.00 | Teacher 1 |
| Student 1 | Rewarding assignment | 60.00 | 80.00 | Teacher 2 |
# Test filtering by grader.
And I set the field "Grader" to "Teacher 1"
And I press "Submit"
And the following should exist in the "gradereport_history" table:
| First name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader |
| Student 1 | student1@example.com | apple | The greatest assignment ever | | 50.00 | Teacher 1 |
And the following should not exist in the "gradereport_history" table:
| Student 1 | The greatest assignment ever | 50.00 | 70.00 | Teacher 2 |
# Test filtering by revised grades.
And I click on "id_revisedonly" "checkbox"
And I press "Submit"
And the following should exist in the "gradereport_history" table:
| First name | Email address | Favourite food | Grade item | Original grade | Revised grade | Grader |
| Student 1 | student1@example.com | apple | The greatest assignment ever | | 50.00 | Teacher 1 |
+652
View File
@@ -0,0 +1,652 @@
<?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/>.
namespace gradereport_history;
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
*/
final class report_test extends \advanced_testcase {
/**
* Create some grades.
*/
public function test_query_db(): void {
$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();
self::getDataGenerator()->enrol_user($grader1->id, $c1->id, 'teacher');
self::getDataGenerator()->enrol_user($grader2->id, $c1->id, 'teacher');
self::getDataGenerator()->enrol_user($u2->id, $c1->id, 'student');
self::getDataGenerator()->enrol_user($u3->id, $c1->id, 'student');
self::getDataGenerator()->enrol_user($u4->id, $c1->id, 'student');
self::getDataGenerator()->enrol_user($u5->id, $c1->id, 'student');
self::getDataGenerator()->enrol_user($grader1->id, $c2->id, 'teacher');
self::getDataGenerator()->enrol_user($u5->id, $c2->id, 'student');
// 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();
$this->setUser($grader1);
$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 the current user cannot access should return all records.
$this->assertEquals(8, $this->get_tablelog_results($c1ctx, array('userids' => $u1->id), true));
// Filtering on 2 users, only one of whom the current user can access.
$this->assertEquals(1, $this->get_tablelog_results($c1ctx, ['userids' => "$u1->id,$u3->id"], true));
$results = $this->get_tablelog_results($c1ctx, ['userids' => "$u1->id,$u3->id"]);
$this->assertGradeHistoryIds([$grades['c1m1u3']->id], $results);
// Filtering on 2 users, both of whom the current user can access.
$this->assertEquals(3, $this->get_tablelog_results($c1ctx, ['userids' => "$u2->id,$u3->id"], true));
$results = $this->get_tablelog_results($c1ctx, ['userids' => "$u2->id,$u3->id"]);
$this->assertGradeHistoryIds([$grades['c1m1u2']->id, $grades['c1m1u3']->id, $grades['c1m2u2']->id], $results);
// 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);
// Put course in separate groups mode, add grader1 and two students to the same group.
$c1->groupmode = SEPARATEGROUPS;
update_course($c1);
$this->assertFalse(has_capability('moodle/site:accessallgroups', \context_course::instance($c1->id)));
$g1 = self::getDataGenerator()->create_group(['courseid' => $c1->id, 'name' => 'g1']);
self::getDataGenerator()->create_group_member(['groupid' => $g1->id, 'userid' => $grader1->id]);
self::getDataGenerator()->create_group_member(['groupid' => $g1->id, 'userid' => $u1->id]);
self::getDataGenerator()->create_group_member(['groupid' => $g1->id, 'userid' => $u2->id]);
$this->assertEquals(2, $this->get_tablelog_results($c1ctx, array(), true));
// Grader2 is not in any groups.
$this->setUser($grader2);
$this->assertEquals(0, $this->get_tablelog_results($c1ctx, array(), true));
}
/**
* Test the get users helper method.
*/
public function test_get_users(): void {
$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'));
}
/**
* Data provider for \gradereport_history_report_testcase::test_get_users_with_profile_fields()
* Testing get_users() and get_users_count() test cases.
*
* @return array List of data sets (test cases)
*/
public static function get_users_with_profile_fields_provider(): array {
return [
// User identity check boxes, 'email', 'profile_field_lang' and 'profile_field_height' are checked.
'show email,lang and height;search for all users' =>
['email,profile_field_lang,profile_field_height', '', ['u1', 'u2', 'u3', 'u4']],
'show email,lang and height;search for users on .org ' =>
['email,profile_field_lang,profile_field_height', '.org', ['u1', 'u2', 'u4']],
'show email,lang and height;search for users on .com ' =>
['email,profile_field_lang,profile_field_height', '.com', []],
'show email,lang and height;search for users on .uk ' =>
['email,profile_field_lang,profile_field_height', '.uk', ['u3']],
'show email,lang and height,search for Spanish speakers' =>
['email,profile_field_lang,profile_field_height', 'spanish', ['u1', 'u4']],
'show email,lang and height,search for Spanish speakers (using spa)' =>
['email,profile_field_lang,profile_field_height', 'spa', ['u1', 'u4']],
'show email,lang and height,search for German speakers' =>
['email,profile_field_lang,profile_field_height', 'german', ['u2']],
'show email,lang and height,search for German speakers (using ger)' =>
['email,profile_field_lang,profile_field_height', 'ger', ['u2']],
'show email,lang and height,search for English speakers' =>
['email,profile_field_lang,profile_field_height', 'english', ['u3']],
'show email,lang and height,search for English speakers (using eng)' =>
['email,profile_field_lang,profile_field_height', 'eng', ['u3']],
'show email,lang and height,search for users with height 180cm' =>
['email,profile_field_lang,profile_field_height', '180', ['u2', 'u3', 'u4']],
'show email,lang and height,search for users with height 170cm' =>
['email,profile_field_lang,profile_field_height', '170', ['u1']],
// User identity check boxes, 'email' and 'profile_field_height' are checked.
'show email and height;search for users on .org' =>
['email,profile_field_height', '.org', ['u1', 'u2', 'u4']],
'show email and height;search for users on .com' =>
['email,profile_field_height', '.com', []],
'show email and height;search for users on .co' =>
['email,profile_field_height', '.co', ['u3']],
'show email and height,search for Spanish speakers' =>
['email,profile_field_height', 'spanish', []],
'show email and height,search for German speakers' =>
['email,profile_field_height', 'german', []],
'show email and height,search for English speakers' =>
['email,profile_field_height', 'english', []],
'show email and height,search for users with height 180cm' =>
['email,profile_field_height', '180', ['u2', 'u3', 'u4']],
'show email and height,search for users with height 170cm' =>
['email,profile_field_height', '170', ['u1']],
// User identity check boxes, only 'email' is checked.
'show email only;search for users on .org' => ['email', '.org', ['u1', 'u2', 'u4']],
'show email only;search for users on .com' => ['email', '.com', []],
'show email only;search for users on .co.uk' => ['email', 'co.uk', ['u3']],
'show email only;search for users on .uk' => ['email', '.uk', ['u3']],
'show email only;search for users on .co' => ['email', '.co', ['u3']],
'show email only;search for Spanish speakers' => ['email', 'spanish', []],
'show email only;search for German speakers' => ['email', 'german', []],
'show email only;search for English speakers' => ['email', 'english', []],
'show email only;search for users with height 180cm' => ['email', '180', []],
'show email only;search for users with height 170cm' => ['email', '170', []],
];
}
/**
* Testing the search functionality on get_users() and get_users_count() and their inner methods.
*
* @dataProvider get_users_with_profile_fields_provider
*
* @param string $showuseridentity, list of user identities to be shown.
* @param string $searchstring, the string to be searched.
* @param array $expectedusernames, a list of expected usernames.
* @return void
*/
public function test_get_users_with_profile_fields(string $showuseridentity, string $searchstring,
array $expectedusernames): void {
global $CFG, $DB;
require_once($CFG->dirroot . '/user/profile/lib.php');
$this->resetAfterTest();
// Create a couple of custom profile fields, which are in user identity.
$generator = $this->getDataGenerator();
$generator->create_custom_profile_field(['datatype' => 'text',
'shortname' => 'lang', 'name' => 'Language']);
$generator->create_custom_profile_field(['datatype' => 'text',
'shortname' => 'height', 'name' => 'Height']);
// Create a couple of test users.
$u1 = $generator->create_user(['firstname' => 'Eduardo', 'lastname' => 'Gomes',
'username' => 'u1', 'email' => 'u1@x.org', 'profile_field_lang' => 'Spanish',
'profile_field_height' => '170cm']);
$u2 = $generator->create_user(['firstname' => 'Dieter', 'lastname' => 'Schmitt',
'username' => 'u2', 'email' => 'u2@x.org', 'profile_field_lang' => 'German',
'profile_field_height' => '180cm']);
$u3 = $generator->create_user(['firstname' => 'Peter', 'lastname' => 'Jones',
'username' => 'u3', 'email' => 'u3@x.co.uk', 'profile_field_lang' => 'English',
'profile_field_height' => '180cm']);
$u4 = $generator->create_user(['firstname' => 'Pedro', 'lastname' => 'Gomes',
'username' => 'u4', 'email' => 'u3@x.org', 'profile_field_lang' => 'Spanish',
'profile_field_height' => '180cm']);
// Do this as admin user.
$this->setAdminUser();
// Making the setup.
$c1 = $this->getDataGenerator()->create_course();
$c1ctx = \context_course::instance($c1->id);
$c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
// 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));
$this->create_grade_history(array('itemid' => $gi->id, 'userid' => $u4->id));
// Checking fetching some users with this config settings.
set_config('showuseridentity', $showuseridentity);
$numberofexpectedusers = count($expectedusernames);
$users = \gradereport_history\helper::get_users($c1ctx, $searchstring);
$userscount = \gradereport_history\helper::get_users_count($c1ctx, $searchstring);
$this->assertEquals($numberofexpectedusers, $userscount);
$this->assertCount($numberofexpectedusers, $users);
foreach ($users as $user) {
if (in_array($user->username, $expectedusernames)) {
$this->assertArrayHasKey($user->id, $users);
} else {
$this->assertArrayNotHasKey($user->id, $users);
}
}
}
/**
* Data provider method for \gradereport_history_report_testcase::test_get_users_with_groups()
*/
public static function get_users_provider(): array {
return [
'Visible groups, non-editing teacher, not in any group' => [
VISIBLEGROUPS, 'teacher', ['g1', 'g2'], ['s1', 's2', 's3', 's4', 's5']
],
'Visible groups, non-editing teacher' => [
VISIBLEGROUPS, 'teacher', [], ['s1', 's2', 's3', 's4', 's5']
],
'Visible groups, editing teacher' => [
VISIBLEGROUPS, 'editingteacher', ['g1', 'g2'], ['s1', 's2', 's3', 's4', 's5']
],
'Separate groups, non-editing teacher' => [
SEPARATEGROUPS, 'teacher', ['g1', 'g2'], ['s1', 's2']
],
'Separate groups, non-editing teacher, not in any group' => [
SEPARATEGROUPS, 'teacher', [], []
],
'Separate groups, non-editing teacher and student share two groups' => [
SEPARATEGROUPS, 'teacher', ['g4', 'g5'], ['s5']
],
'Separate groups, editing teacher' => [
SEPARATEGROUPS, 'editingteacher', ['g1', 'g2'], ['s1', 's2', 's3', 's4', 's5']
],
];
}
/**
* Test for helper::get_users() with course group mode set.
*
* @dataProvider get_users_provider
* @param $groupmode
* @param $teacherrole
* @param $teachergroups
* @param $expectedusers
*/
public function test_get_users_with_groups($groupmode, $teacherrole, $teachergroups, $expectedusers): void {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator();
// Create a test course.
$course = $generator->create_course(['groupmode' => $groupmode]);
// Create an assignment module.
$assign = $generator->create_module('assign', ['course' => $course]);
// Fetch roles.
$role = $DB->get_record('role', ['shortname' => $teacherrole], '*', MUST_EXIST);
$studentrole = $DB->get_record('role', ['shortname' => 'student'], '*', MUST_EXIST);
// Create users.
$t1 = $generator->create_user(['username' => 't1', 'email' => 't1@example.com']);
$s1 = $generator->create_user(['username' => 's1', 'email' => 's1@example.com']);
$s2 = $generator->create_user(['username' => 's2', 'email' => 's2@example.com']);
$s3 = $generator->create_user(['username' => 's3', 'email' => 's3@example.com']);
$s4 = $generator->create_user(['username' => 's4', 'email' => 's4@example.com']);
$s5 = $generator->create_user(['username' => 's5', 'email' => 's5@example.com']);
// Enrol users.
$generator->enrol_user($t1->id, $course->id, $role->id);
$generator->enrol_user($s1->id, $course->id, $studentrole->id);
$generator->enrol_user($s2->id, $course->id, $studentrole->id);
$generator->enrol_user($s3->id, $course->id, $studentrole->id);
$generator->enrol_user($s4->id, $course->id, $studentrole->id);
$generator->enrol_user($s5->id, $course->id, $studentrole->id);
// Create groups.
$groups = [];
$groups['g1'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g1']);
$groups['g2'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g2']);
$groups['g3'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g3']);
$groups['g4'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g4']);
$groups['g5'] = $generator->create_group(['courseid' => $course->id, 'name' => 'g5']);
// Add teacher to the assigned groups.
foreach ($teachergroups as $groupname) {
$group = $groups[$groupname];
$generator->create_group_member(['groupid' => $group->id, 'userid' => $t1->id]);
}
// Add students to groups.
$generator->create_group_member(['groupid' => $groups['g1']->id, 'userid' => $s1->id]);
$generator->create_group_member(['groupid' => $groups['g2']->id, 'userid' => $s2->id]);
$generator->create_group_member(['groupid' => $groups['g3']->id, 'userid' => $s3->id]);
$generator->create_group_member(['groupid' => $groups['g4']->id, 'userid' => $s5->id]);
$generator->create_group_member(['groupid' => $groups['g5']->id, 'userid' => $s5->id]);
// Creating grade history for the students.
$gi = \grade_item::fetch(['iteminstance' => $assign->id, 'itemtype' => 'mod', 'itemmodule' => 'assign']);
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s1->id]);
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s2->id]);
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s3->id]);
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s4->id]);
$this->create_grade_history(['itemid' => $gi->id, 'userid' => $s5->id]);
// Log in as the teacher.
$this->setUser($t1);
// Fetch the users.
$users = \gradereport_history\helper::get_users(\context_course::instance($course->id));
// Confirm that the number of users fetched is the same as the count of expected users.
$this->assertCount(count($expectedusers), $users);
foreach ($users as $user) {
// Confirm that each user returned is in the list of expected users.
$this->assertTrue(in_array($user->username, $expectedusers));
}
}
/**
* Test the get graders helper method.
*/
public function test_graders(): void {
$this->resetAfterTest();
// Making the setup.
$c1 = $this->getDataGenerator()->create_course();
$c2 = $this->getDataGenerator()->create_course();
$c3 = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS]);
$c1m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c1));
$c2m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c2));
$c3m1 = $this->getDataGenerator()->create_module('assign', array('course' => $c3));
// 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'));
foreach ([$c1, $c2, $c3] as $course) {
foreach ([$u1, $u2, $u3, $u4] as $user) {
self::getDataGenerator()->enrol_user($user->id, $course->id, 'student');
}
}
// 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));
$gi = \grade_item::fetch(array('iteminstance' => $c3m1->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' => $u2->id, 'usermodified' => $u2->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);
// Third course is in separate groups mode. Only graders from the same group will be returned.
$g = self::getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'g1']);
self::getDataGenerator()->create_group_member(['groupid' => $g->id, 'userid' => $u1->id]);
self::getDataGenerator()->create_group_member(['groupid' => $g->id, 'userid' => $u2->id]);
$this->setUser($u1);
$graders = \gradereport_history\helper::get_graders($c3->id);
$this->assertCount(3, $graders); // Including "all graders" .
$this->setUser($u3);
$graders = \gradereport_history\helper::get_graders($c3->id);
$this->assertCount(1, $graders); // Including "all 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);
}
}
}
+77
View File
@@ -0,0 +1,77 @@
<?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);;
$userfieldsapi = \core_user\fields::for_identity($context)->with_userpic();
$extrafields = $userfieldsapi->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]);
$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 = [];
foreach ($extrafields as $field) {
if ($user->{$field}) {
$fieldvalues[] = $user->{$field};
}
}
$newuser->extrafields = implode(', ', $fieldvalues);
$outcome->response['users'][] = $newuser;
}
$outcome->success = true;
echo $OUTPUT->header();
echo json_encode($outcome);
echo $OUTPUT->footer();
+30
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 = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'gradereport_history';
@@ -0,0 +1,873 @@
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: window.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 = '';
var user;
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');
}
if (!user) {
// We were not passed a user, there is apparently none in the dialogue. Nothing to do here \\\o/.
return;
}
this._userTabFocus = user.one(SELECTORS.USERSELECT);
this._userTabFocus.setAttribute('tabindex', '0');
this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
}
}, {
NAME: USP.NAME,
CSS_PREFIX: USP.CSS_PREFIX,
ATTRS: {
/**
* The header.
*
* @attribute title
* @default selectusers language string.
* @type String
*/
title: {
validator: Y.Lang.isString,
valueFn: function() {
return M.util.get_string('selectusers', COMPONENT);
}
},
/**
* The current page URL.
*
* @attribute url
* @default null
* @type String
*/
url: {
validator: Y.Lang.isString,
value: null
},
/**
* The URL to the Ajax file.
*
* @attribute ajaxurl
* @default null
* @type String
*/
ajaxurl: {
validator: Y.Lang.isString,
value: null
},
/**
* The names of the selected users.
*
* The keys are the user IDs, the values are their fullname.
*
* @attribute selectedUsers
* @default null
* @type Object
*/
selectedUsers: {
validator: Y.Lang.isObject,
value: null,
getter: function(v) {
if (v === null) {
return {};
}
return v;
}
},
/**
* The course ID.
*
* @attribute courseid
* @default null
* @type Number
*/
courseid: {
value: null
},
/**
* Array of parameters.
*
* @attribute params
* @default []
* @type Array
*/
params: {
validator: Y.Lang.isArray,
value: []
},
/**
* The page we are on.
*
* @attribute page
* @default 0
* @type Number
*/
page: {
validator: Y.Lang.isNumber,
value: 0
},
/**
* The number of users displayed.
*
* @attribute userCount
* @default 0
* @type Number
*/
userCount: {
value: 0,
validator: Y.Lang.isNumber
},
/**
* The number of results per page.
*
* @attribute perPage
* @default 25
* @type Number
*/
perPage: {
value: 25,
Validator: Y.Lang.isNumber
}
}
});
Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {
/**
* List of extra classes.
*
* @attribute extraClasses
* @default ['gradereport_history_usp']
* @type Array
*/
extraClasses: {
value: [
'gradereport_history_usp'
]
},
/**
* Whether to focus on the target that caused the Widget to be shown.
*
* @attribute focusOnPreviousTargetAfterHide
* @default true
* @type Node
*/
focusOnPreviousTargetAfterHide: {
value: true
},
/**
*
* Width.
*
* @attribute width
* @default '500px'
* @type String|Number
*/
width: {
value: '500px'
},
/**
* Boolean indicating whether or not the Widget is visible.
*
* @attribute visible
* @default false
* @type Boolean
*/
visible: {
value: false
},
/**
* Whether the widget should be modal or not.
*
* @attribute modal
* @type Boolean
* @default true
*/
modal: {
value: true
},
/**
* Whether the widget should be draggable or not.
*
* @attribute draggable
* @type Boolean
* @default true
*/
draggable: {
value: true
}
});
Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) {
return new USERSELECTOR(cfg);
};
}, '@VERSION@', {
"requires": [
"escape",
"event-delegate",
"event-key",
"handlebars",
"io-base",
"json-parse",
"moodle-core-notification-dialogue"
]
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,871 @@
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: window.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 = '';
var user;
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');
}
if (!user) {
// We were not passed a user, there is apparently none in the dialogue. Nothing to do here \\\o/.
return;
}
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"
]
});
@@ -0,0 +1,10 @@
{
"name": "moodle-gradereport_history-userselector",
"builds": {
"moodle-gradereport_history-userselector": {
"jsfiles": [
"userselector.js"
]
}
}
}
@@ -0,0 +1,858 @@
// 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: window.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 = '';
var user;
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');
}
if (!user) {
// We were not passed a user, there is apparently none in the dialogue. Nothing to do here \\\o/.
return;
}
this._userTabFocus = user.one(SELECTORS.USERSELECT);
this._userTabFocus.setAttribute('tabindex', '0');
this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
}
}, {
NAME: USP.NAME,
CSS_PREFIX: USP.CSS_PREFIX,
ATTRS: {
/**
* The header.
*
* @attribute title
* @default selectusers language string.
* @type String
*/
title: {
validator: Y.Lang.isString,
valueFn: function() {
return M.util.get_string('selectusers', COMPONENT);
}
},
/**
* The current page URL.
*
* @attribute url
* @default null
* @type String
*/
url: {
validator: Y.Lang.isString,
value: null
},
/**
* The URL to the Ajax file.
*
* @attribute ajaxurl
* @default null
* @type String
*/
ajaxurl: {
validator: Y.Lang.isString,
value: null
},
/**
* The names of the selected users.
*
* The keys are the user IDs, the values are their fullname.
*
* @attribute selectedUsers
* @default null
* @type Object
*/
selectedUsers: {
validator: Y.Lang.isObject,
value: null,
getter: function(v) {
if (v === null) {
return {};
}
return v;
}
},
/**
* The course ID.
*
* @attribute courseid
* @default null
* @type Number
*/
courseid: {
value: null
},
/**
* Array of parameters.
*
* @attribute params
* @default []
* @type Array
*/
params: {
validator: Y.Lang.isArray,
value: []
},
/**
* The page we are on.
*
* @attribute page
* @default 0
* @type Number
*/
page: {
validator: Y.Lang.isNumber,
value: 0
},
/**
* The number of users displayed.
*
* @attribute userCount
* @default 0
* @type Number
*/
userCount: {
value: 0,
validator: Y.Lang.isNumber
},
/**
* The number of results per page.
*
* @attribute perPage
* @default 25
* @type Number
*/
perPage: {
value: 25,
Validator: Y.Lang.isNumber
}
}
});
Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {
/**
* List of extra classes.
*
* @attribute extraClasses
* @default ['gradereport_history_usp']
* @type Array
*/
extraClasses: {
value: [
'gradereport_history_usp'
]
},
/**
* Whether to focus on the target that caused the Widget to be shown.
*
* @attribute focusOnPreviousTargetAfterHide
* @default true
* @type Node
*/
focusOnPreviousTargetAfterHide: {
value: true
},
/**
*
* Width.
*
* @attribute width
* @default '500px'
* @type String|Number
*/
width: {
value: '500px'
},
/**
* Boolean indicating whether or not the Widget is visible.
*
* @attribute visible
* @default false
* @type Boolean
*/
visible: {
value: false
},
/**
* Whether the widget should be modal or not.
*
* @attribute modal
* @type Boolean
* @default true
*/
modal: {
value: true
},
/**
* Whether the widget should be draggable or not.
*
* @attribute draggable
* @type Boolean
* @default true
*/
draggable: {
value: true
}
});
Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) {
return new USERSELECTOR(cfg);
};
@@ -0,0 +1,13 @@
{
"moodle-gradereport_history-userselector": {
"requires": [
"escape",
"event-delegate",
"event-key",
"handlebars",
"io-base",
"json-parse",
"moodle-core-notification-dialogue"
]
}
}