MDL-80744 mod_assign: Add user search.

This commit is contained in:
Ilya Tregubov 2024-06-12 17:12:59 +08:00
parent 8087893fcb
commit de54a993d3
16 changed files with 656 additions and 97 deletions

View file

@ -117,7 +117,13 @@ class action_bar extends \core_grades\output\action_bar {
}
$resetlink = new moodle_url('/grade/report/grader/index.php', ['id' => $courseid]);
$userselectorrenderer = new \core_course\output\actionbar\user_selector($course, $resetlink, $this->userid, 0, $this->usersearch);
$userselectorrenderer = new \core_course\output\actionbar\user_selector(
course: $course,
resetlink: $resetlink,
userid: $this->userid,
groupid: 0,
usersearch: $this->usersearch
);
$data['searchdropdown'] = $userselectorrenderer->export_for_template($output);
// The collapsed column dialog is aligned to the edge of the screen, we need to place it such that it also aligns.
$collapsemenudirection = right_to_left() ? 'dropdown-menu-left' : 'dropdown-menu-right';

View file

@ -485,108 +485,15 @@ abstract class grade_report {
// A user wants to return a subset of learners that match their search criteria.
if ($this->usersearch !== '' && $this->userid === -1) {
// Get the fields for all contexts because there is a special case later where it allows
// matches of fields you can't access if they are on your own account.
$userfields = fields::for_identity(null, false)->with_userpic();
['mappings' => $mappings] = (array)$userfields->get_sql('u', true);
[
'where' => $keywordswhere,
'params' => $keywordsparams,
] = $this->get_users_search_sql($mappings, $userfields->get_required_fields());
] = \core_user::get_users_search_sql($this->context, $this->usersearch);
$this->userwheresql .= " AND $keywordswhere";
$this->userwheresql_params = array_merge($this->userwheresql_params, $keywordsparams);
}
}
/**
* Prepare SQL where clause and associated parameters for any user searching being performed.
* This mostly came from core_user\table\participants_search with some slight modifications four our use case.
*
* @param array $mappings Array of field mappings (fieldname => SQL code for the value)
* @param array $userfields An array that we cast from user profile fields to search within.
* @return array SQL query data in the format ['where' => '', 'params' => []].
*/
protected function get_users_search_sql(array $mappings, array $userfields): array {
global $DB, $USER;
$canviewfullnames = has_capability('moodle/site:viewfullnames', $this->context);
$params = [];
$searchkey1 = 'search01';
$searchkey2 = 'search02';
$searchkey3 = 'search03';
$conditions = [];
// Search by fullname.
[$fullname, $fullnameparams] = fields::get_sql_fullname('u', $canviewfullnames);
$conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
$params = array_merge($params, $fullnameparams);
// Search by email.
$email = $DB->sql_like('email', ':' . $searchkey2, false, false);
if (!in_array('email', $userfields)) {
$maildisplay = 'maildisplay0';
$userid1 = 'userid01';
// Prevent users who hide their email address from being found by others
// who aren't allowed to see hidden email addresses.
$email = "(". $email ." AND (" .
"u.maildisplay <> :$maildisplay " .
"OR u.id = :$userid1". // Users can always find themselves.
"))";
$params[$maildisplay] = core_user::MAILDISPLAY_HIDE;
$params[$userid1] = $USER->id;
}
$conditions[] = $email;
// Search by idnumber.
$idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false);
if (!in_array('idnumber', $userfields)) {
$userid2 = 'userid02';
// Users who aren't allowed to see idnumbers should at most find themselves
// when searching for an idnumber.
$idnumber = "(". $idnumber . " AND u.id = :$userid2)";
$params[$userid2] = $USER->id;
}
$conditions[] = $idnumber;
// Search all user identify fields.
$extrasearchfields = fields::get_identity_fields(null, false);
foreach ($extrasearchfields as $fieldindex => $extrasearchfield) {
if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
// Already covered above.
continue;
}
// The param must be short (max 32 characters) so don't include field name.
$param = $searchkey3 . '_ident' . $fieldindex;
$fieldsql = $mappings[$extrasearchfield];
$condition = $DB->sql_like($fieldsql, ':' . $param, false, false);
$params[$param] = "%$this->usersearch%";
if (!in_array($extrasearchfield, $userfields)) {
// User cannot see this field, but allow match if their own account.
$userid3 = 'userid03_ident' . $fieldindex;
$condition = "(". $condition . " AND u.id = :$userid3)";
$params[$userid3] = $USER->id;
}
$conditions[] = $condition;
}
$where = "(". implode(" OR ", $conditions) .") ";
$params[$searchkey1] = "%$this->usersearch%";
$params[$searchkey2] = "%$this->usersearch%";
$params[$searchkey3] = "%$this->usersearch%";
return [
'where' => $where,
'params' => $params,
];
}
/**
* Returns an arrow icon inside an <a> tag, for the purpose of sorting a column.
* @param string $direction

View file

@ -99,7 +99,12 @@ class action_bar extends \core_grades\output\action_bar {
}
$data['userselector'] = [
'courseid' => $courseid,
'content' => $userreportrenderer->users_selector(get_course($courseid), $this->userid, $this->currentgroupid, $this->usersearch)
'content' => $userreportrenderer->users_selector(
course: get_course($courseid),
userid: $this->userid,
groupid: $this->currentgroupid,
usersearch: $this->usersearch
),
];
// Do not output the 'view mode' selector when in zero state or when the current user is viewing its own report.

View file

@ -22,6 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_user\fields;
defined('MOODLE_INTERNAL') || die();
/**
@ -1489,4 +1491,97 @@ class core_user {
return $initials;
}
/**
* Prepare SQL where clause and associated parameters for any user searching being performed.
* This mostly came from core_user\table\participants_search with some slight modifications four our use case.
*
* @param context $context Context we are in.
* @param string $usersearch Array of field mappings (fieldname => SQL code for the value)
* @return array SQL query data in the format ['where' => '', 'params' => []].
*/
public static function get_users_search_sql(context $context, string $usersearch = ''): array {
global $DB, $USER;
$userfields = fields::for_identity($context, false)->with_userpic();
['mappings' => $mappings] = (array)$userfields->get_sql('u', true);
$userfields = $userfields->get_required_fields();
$canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
$params = [];
$searchkey1 = 'search01';
$searchkey2 = 'search02';
$searchkey3 = 'search03';
$conditions = [];
// Search by fullname.
[$fullname, $fullnameparams] = fields::get_sql_fullname('u', $canviewfullnames);
$conditions[] = $DB->sql_like($fullname, ':' . $searchkey1, false, false);
$params = array_merge($params, $fullnameparams);
// Search by email.
$email = $DB->sql_like('email', ':' . $searchkey2, false, false);
if (!in_array('email', $userfields)) {
$maildisplay = 'maildisplay0';
$userid1 = 'userid01';
// Prevent users who hide their email address from being found by others
// who aren't allowed to see hidden email addresses.
$email = "(". $email ." AND (" .
"u.maildisplay <> :$maildisplay " .
"OR u.id = :$userid1". // Users can always find themselves.
"))";
$params[$maildisplay] = self::MAILDISPLAY_HIDE;
$params[$userid1] = $USER->id;
}
$conditions[] = $email;
// Search by idnumber.
$idnumber = $DB->sql_like('idnumber', ':' . $searchkey3, false, false);
if (!in_array('idnumber', $userfields)) {
$userid2 = 'userid02';
// Users who aren't allowed to see idnumbers should at most find themselves
// when searching for an idnumber.
$idnumber = "(". $idnumber . " AND u.id = :$userid2)";
$params[$userid2] = $USER->id;
}
$conditions[] = $idnumber;
// Search all user identify fields.
$extrasearchfields = fields::get_identity_fields(null, false);
foreach ($extrasearchfields as $fieldindex => $extrasearchfield) {
if (in_array($extrasearchfield, ['email', 'idnumber', 'country'])) {
// Already covered above.
continue;
}
// The param must be short (max 32 characters) so don't include field name.
$param = $searchkey3 . '_ident' . $fieldindex;
$fieldsql = $mappings[$extrasearchfield];
$condition = $DB->sql_like($fieldsql, ':' . $param, false, false);
$params[$param] = "%$usersearch%";
if (!in_array($extrasearchfield, $userfields)) {
// User cannot see this field, but allow match if their own account.
$userid3 = 'userid03_ident' . $fieldindex;
$condition = "(". $condition . " AND u.id = :$userid3)";
$params[$userid3] = $USER->id;
}
$conditions[] = $condition;
}
$where = "(". implode(" OR ", $conditions) .") ";
$params[$searchkey1] = "%$usersearch%";
$params[$searchkey2] = "%$usersearch%";
$params[$searchkey3] = "%$usersearch%";
return [
'where' => $where,
'params' => $params,
];
}
}

10
mod/assign/amd/build/repository.min.js vendored Normal file
View file

@ -0,0 +1,10 @@
define("mod_assign/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A repo for the search partial in the submissions page.
*
* @module mod_assign/repository
* @copyright 2024 Ilya Tregubov <ilyatregubov@proton.me>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.userFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.userFetch=(assignid,groupid)=>{const request={methodname:"mod_assign_list_participants",args:{assignid:assignid,groupid:groupid,filter:""}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the search partial in the submissions page.\n *\n * @module mod_assign/repository\n * @copyright 2024 Ilya Tregubov <ilyatregubov@proton.me>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\n\n/**\n * Given a course ID, we want to fetch the learners within this assignment.\n *\n * @method userFetch\n * @param {int} assignid ID of the assignment.\n * @param {int} groupid ID of the selected group.\n * @return {object} jQuery promise\n */\nexport const userFetch = (assignid, groupid) => {\n const request = {\n methodname: 'mod_assign_list_participants',\n args: {\n assignid: assignid,\n groupid: groupid,\n filter: '',\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["assignid","groupid","request","methodname","args","filter","ajax","call"],"mappings":";;;;;;;8JAiCyB,CAACA,SAAUC,iBAC1BC,QAAU,CACZC,WAAY,+BACZC,KAAM,CACFJ,SAAUA,SACVC,QAASA,QACTI,OAAQ,YAGTC,cAAKC,KAAK,CAACL,UAAU"}

11
mod/assign/amd/build/user.min.js vendored Normal file
View file

@ -0,0 +1,11 @@
define("mod_assign/user",["exports","core_user/comboboxsearch/user","mod_assign/repository"],(function(_exports,_user,Repository){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_user=(obj=_user)&&obj.__esModule?obj:{default:obj},Repository=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Repository);const selectors_component=".user-search",selectors_groupid='[data-region="groupid"]',selectors_instance='[data-region="instance"]',component=document.querySelector(selectors_component),groupID=parseInt(component.querySelector(selectors_groupid).dataset.groupid,10),assignID=parseInt(component.querySelector(selectors_instance).dataset.instance,10);
/**
* Allow the user to search for users in the action bar.
*
* @module mod_assign/user
* @copyright 2024 Ilya Tregubov <ilyatregubov@proton.me>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class User extends _user.default{constructor(baseUrl){super(),this.baseUrl=baseUrl}static init(baseUrl){return new User(baseUrl)}selectAllResultsLink(){const url=new URL(this.baseUrl);return url.searchParams.set("search",this.getSearchTerm()),url.toString()}fetchDataset(){return Repository.userFetch(assignID,groupID).then((r=>r))}selectOneLink(userID){const url=new URL(this.baseUrl);return url.searchParams.set("search",this.getSearchTerm()),url.searchParams.set("userid",userID.toString()),url.toString()}}return _exports.default=User,_exports.default}));
//# sourceMappingURL=user.min.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"user.min.js","sources":["../src/user.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\nimport UserSearch from 'core_user/comboboxsearch/user';\nimport * as Repository from 'mod_assign/repository';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n groupid: '[data-region=\"groupid\"]',\n instance: '[data-region=\"instance\"]',\n currentvalue: '[data-region=\"currentvalue\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst groupID = parseInt(component.querySelector(selectors.groupid).dataset.groupid, 10);\nconst assignID = parseInt(component.querySelector(selectors.instance).dataset.instance, 10);\n\n/**\n * Allow the user to search for users in the action bar.\n *\n * @module mod_assign/user\n * @copyright 2024 Ilya Tregubov <ilyatregubov@proton.me>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default class User extends UserSearch {\n\n /**\n * Construct the class.\n *\n * @param {string} baseUrl The base URL for the page.\n */\n constructor(baseUrl) {\n super();\n this.baseUrl = baseUrl;\n }\n\n /**\n * Allow the class to be invoked via PHP.\n *\n * @param {string} baseUrl The base URL for the page.\n * @returns {User}\n */\n static init(baseUrl) {\n return new User(baseUrl);\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n const url = new URL(this.baseUrl);\n url.searchParams.set('search', this.getSearchTerm());\n\n return url.toString();\n }\n\n /**\n * Get the data we will be searching against in this component.\n *\n * @returns {Promise<*>}\n */\n fetchDataset() {\n return Repository.userFetch(assignID, groupID).then((r) => r);\n }\n\n /**\n * Build up the link that is dedicated to a particular result.\n *\n * @param {Number} userID The ID of the user selected.\n * @returns {string|*}\n */\n selectOneLink(userID) {\n const url = new URL(this.baseUrl);\n url.searchParams.set('search', this.getSearchTerm());\n url.searchParams.set('userid', userID.toString());\n\n return url.toString();\n }\n}\n"],"names":["selectors","component","document","querySelector","groupID","parseInt","dataset","groupid","assignID","instance","User","UserSearch","constructor","baseUrl","selectAllResultsLink","url","URL","this","searchParams","set","getSearchTerm","toString","fetchDataset","Repository","userFetch","then","r","selectOneLink","userID"],"mappings":"2sCAmBMA,oBACS,eADTA,kBAEO,0BAFPA,mBAGQ,2BAGRC,UAAYC,SAASC,cAAcH,qBACnCI,QAAUC,SAASJ,UAAUE,cAAcH,mBAAmBM,QAAQC,QAAS,IAC/EC,SAAWH,SAASJ,UAAUE,cAAcH,oBAAoBM,QAAQG,SAAU;;;;;;;;MASnEC,aAAaC,cAO9BC,YAAYC,sBAEHA,QAAUA,oBASPA,gBACD,IAAIH,KAAKG,SAQpBC,6BACUC,IAAM,IAAIC,IAAIC,KAAKJ,gBACzBE,IAAIG,aAAaC,IAAI,SAAUF,KAAKG,iBAE7BL,IAAIM,WAQfC,sBACWC,WAAWC,UAAUhB,SAAUJ,SAASqB,MAAMC,GAAMA,IAS/DC,cAAcC,cACJb,IAAM,IAAIC,IAAIC,KAAKJ,gBACzBE,IAAIG,aAAaC,IAAI,SAAUF,KAAKG,iBACpCL,IAAIG,aAAaC,IAAI,SAAUS,OAAOP,YAE/BN,IAAIM"}

View file

@ -0,0 +1,44 @@
// 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/>.
/**
* A repo for the search partial in the submissions page.
*
* @module mod_assign/repository
* @copyright 2024 Ilya Tregubov <ilyatregubov@proton.me>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ajax from 'core/ajax';
/**
* Given a course ID, we want to fetch the learners within this assignment.
*
* @method userFetch
* @param {int} assignid ID of the assignment.
* @param {int} groupid ID of the selected group.
* @return {object} jQuery promise
*/
export const userFetch = (assignid, groupid) => {
const request = {
methodname: 'mod_assign_list_participants',
args: {
assignid: assignid,
groupid: groupid,
filter: '',
},
};
return ajax.call([request])[0];
};

View file

@ -0,0 +1,93 @@
// 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/>.
import UserSearch from 'core_user/comboboxsearch/user';
import * as Repository from 'mod_assign/repository';
// Define our standard lookups.
const selectors = {
component: '.user-search',
groupid: '[data-region="groupid"]',
instance: '[data-region="instance"]',
currentvalue: '[data-region="currentvalue"]',
};
const component = document.querySelector(selectors.component);
const groupID = parseInt(component.querySelector(selectors.groupid).dataset.groupid, 10);
const assignID = parseInt(component.querySelector(selectors.instance).dataset.instance, 10);
/**
* Allow the user to search for users in the action bar.
*
* @module mod_assign/user
* @copyright 2024 Ilya Tregubov <ilyatregubov@proton.me>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default class User extends UserSearch {
/**
* Construct the class.
*
* @param {string} baseUrl The base URL for the page.
*/
constructor(baseUrl) {
super();
this.baseUrl = baseUrl;
}
/**
* Allow the class to be invoked via PHP.
*
* @param {string} baseUrl The base URL for the page.
* @returns {User}
*/
static init(baseUrl) {
return new User(baseUrl);
}
/**
* Build up the view all link.
*
* @returns {string|*}
*/
selectAllResultsLink() {
const url = new URL(this.baseUrl);
url.searchParams.set('search', this.getSearchTerm());
return url.toString();
}
/**
* Get the data we will be searching against in this component.
*
* @returns {Promise<*>}
*/
fetchDataset() {
return Repository.userFetch(assignID, groupID).then((r) => r);
}
/**
* Build up the link that is dedicated to a particular result.
*
* @param {Number} userID The ID of the user selected.
* @returns {string|*}
*/
selectOneLink(userID) {
const url = new URL(this.baseUrl);
url.searchParams.set('search', this.getSearchTerm());
url.searchParams.set('userid', userID.toString());
return url.toString();
}
}

View file

@ -24,6 +24,8 @@
namespace mod_assign\output;
use assign;
use context_module;
use templatable;
use renderable;
use moodle_url;
@ -71,14 +73,35 @@ class grading_actionmenu implements templatable, renderable {
$course = $PAGE->course;
$data = [];
$context = context_module::instance($this->cmid);
$assign = new assign($context, null, null);
$assignid = $assign->get_instance()->id;
if ($this->submissionpluginenabled && $this->submissioncount) {
$data['downloadall'] = (
new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'downloadall'])
)->out(false);
}
$userid = optional_param('userid', null, PARAM_INT);
// If the user ID is set, it indicates that a user has been selected. In this case, override the user search
// string with the full name of the selected user.
$usersearch = $userid ? fullname(\core_user::get_user($userid)) : optional_param('search', '', PARAM_NOTAGS);
$actionbarrenderer = $PAGE->get_renderer('core_course', 'actionbar');
$resetlink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'grading']);
$groupid = groups_get_course_group($course, true);
$userselector = new \core_course\output\actionbar\user_selector(
course: $course,
resetlink: $resetlink,
userid: $userid,
groupid: $groupid,
usersearch: $usersearch,
instanceid: $assignid
);
$data['userselector'] = $actionbarrenderer->render($userselector);
if ($course->groupmode) {
$actionbarrenderer = $PAGE->get_renderer('core_course', 'actionbar');
$data['groupselector'] = $actionbarrenderer->render(new \core_course\output\actionbar\group_selector($course));
}

View file

@ -124,6 +124,12 @@ class assign_grading_table extends table_sql implements renderable {
$this->rownum = $rowoffset - 1;
}
$userid = optional_param('userid', null, PARAM_INT);
$groupid = groups_get_course_group($assignment->get_course(), true);
// If the user ID is set, it indicates that a user has been selected. In this case, override the user search
// string with the full name of the selected user.
$usersearch = $userid ? fullname(\core_user::get_user($userid)) : optional_param('search', '', PARAM_NOTAGS);
$assignment->set_usersearch($userid, $groupid, $usersearch);
$users = array_keys( $assignment->list_participants($currentgroup, true));
if (count($users) == 0) {
// Insert a record that will never match to the sql is still valid.

View file

@ -208,6 +208,9 @@ class assign {
/** @var float grade value. */
public $grade;
/** @var array $usersearch The content that the current user is looking for. */
protected array $usersearch = [];
/**
* Constructor for the base assign class.
*
@ -337,6 +340,21 @@ class assign {
$this->course = $course;
}
/**
* Set usersearch to limit results when getting list of participants.
*
* @param int|null $userid User id to search for.
* @param int|null $groupid Group id to limit resuts to specific group.
* @param string $usersearch Search string to limit results.
*/
public function set_usersearch(?int $userid, ?int $groupid, string $usersearch = ''): void {
$usersearcharray = [];
$usersearcharray['userid'] = $userid;
$usersearcharray['groupid'] = $groupid;
$usersearcharray['usersearch'] = $usersearch;
$this->usersearch = $usersearcharray;
}
/**
* Set error message.
*
@ -2320,6 +2338,21 @@ class assign {
$params['markerid'] = $USER->id;
}
// A user wants to view a particular user rather than a set of users.
if ($this->usersearch) {
if (isset($this->usersearch['userid'])) {
$additionalfilters .= " AND u.id = :uid";
$params['uid'] = $this->usersearch['userid'];
} else if ($this->usersearch['usersearch'] !== '') { // A user wants to view a subset of learners that match the search criteria.
[
'where' => $keywordswhere,
'params' => $keywordsparams,
] = \core_user::get_users_search_sql($this->context, $this->usersearch['usersearch']);
$additionalfilters .= " AND $keywordswhere";
$params = array_merge($params, $keywordsparams);
}
}
$sql = "SELECT $fields
FROM {user} u
JOIN ($esql UNION $ssql) je ON je.id = u.id
@ -4570,6 +4603,8 @@ class assign {
$currenturl = new moodle_url('/mod/assign/view.php', ['id' => $this->get_course_module()->id, 'action' => 'grading']);
$PAGE->activityheader->set_attrs(['hidecompletion' => true]);
$PAGE->requires->js_call_amd('mod_assign/user', 'init', [$currenturl->out(false)]);
// Conditionally add the group JS if we have groups enabled.
if ($this->get_course()->groupmode) {
$PAGE->requires->js_call_amd('core_course/actionbar/group', 'init', [$currenturl->out(false)]);

View file

@ -23,10 +23,12 @@
* none
Context variables required for this template:
* userselector - HTML that outputs the user selector
* groupselector - (optional) HTML that outputs the group selector
Example context (json):
{
"userselector": "<div class='user-search'></div>",
"groupselector": "<div class='group-selector'></div>",
"pagereset": "http://moodle.local/mod/assign/view.php?id=2&action=grading&group=0",
"downloadall": "https://moodle.org"
@ -39,6 +41,12 @@
<h2>{{#str}}gradeitem:submissions, mod_assign{{/str}}</h2>
</div>
<div class="navitem-divider d-none d-sm-flex"></div>
{{#userselector}}
<div class="navitem">
{{{.}}}
</div>
<div class="navitem-divider d-none d-sm-flex"></div>
{{/userselector}}
{{#groupselector}}
<div class="navitem">
{{{.}}}

View file

@ -0,0 +1,313 @@
@mod @mod_assign @javascript
Feature: Within the assignment submissions page, test that we can search for users
In order to filter specific users in the assignment submissions page
As a teacher
I need to be able to see and trigger the search filter
Background:
Given the following "courses" exist:
| fullname | shortname | category | groupmode | groupmodeforce |
| Course 1 | C1 | 0 | 1 | 1 |
And the following "users" exist:
| username | firstname | lastname | email | idnumber | phone1 | phone2 | department | institution | city | country |
| teacher1 | Teacher | 1 | teacher1@example.com | t1 | 1234567892 | 1234567893 | ABC1 | ABCD | Perth | AU |
| student1 | Student | 1 | student1@example.com | s1 | 3213078612 | 8974325612 | ABC1 | ABCD | Hanoi | VN |
| student2 | Dummy | User | student2@example.com | s2 | 4365899871 | 7654789012 | ABC2 | ABCD | Tokyo | JP |
| student3 | User | Example | student3@example.com | s3 | 3243249087 | 0875421745 | ABC2 | ABCD | Olney | GB |
| student4 | User | Test | student4@example.com | s4 | 0987532523 | 2149871323 | ABC3 | ABCD | Tokyo | JP |
| student5 | Turtle | Manatee | student5@example.com | s5 | 1239087780 | 9873623589 | ABC3 | ABCD | Perth | AU |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
| student4 | C1 | student |
| student5 | C1 | student |
And the following "groups" exist:
| name | course | idnumber |
| Default group | C1 | dg |
| Advanced group | C1 | ag |
And the following "group members" exist:
| user | group |
| student3 | ag |
| student5 | dg |
And the following "activities" exist:
| activity | course | name |
| assign | C1 | Test assignment one |
And the following config values are set as admin:
| showuseridentity | idnumber,email,city,country,phone1,phone2,department,institution |
And I am on the "Test assignment one" Activity page logged in as teacher1
And I follow "View all submissions"
And I change window size to "large"
Scenario: A teacher can view and trigger the user search
# Check the placeholder text
Given I should see "Search users"
# Confirm the search is currently inactive and results are unfiltered.
And the following should exist in the "generaltable" table:
| -1- |
| Turtle Manatee |
| Student 1 |
| User Example |
| User Test |
| Dummy User |
And the following should not exist in the "generaltable" table:
| -1- |
| Teacher 1 |
When I set the field "Search users" to "Turtle"
And I wait until "View all results (1)" "option_role" exists
And I confirm "Turtle Manatee" exists in the "Search users" search combo box
And I confirm "User Example" does not exist in the "Search users" search combo box
And I click on "Turtle Manatee" "list_item"
# Business case: This will trigger a page reload and can not dynamically update the table.
And I wait until the page is ready
Then the following should exist in the "generaltable" table:
| -1- |
| Turtle Manatee |
And the following should not exist in the "generaltable" table:
| -1- |
| Teacher 1 |
| Student 1 |
| User Example |
| User Test |
| Dummy User |
And I set the field "Search users" to "Turt"
And I wait until "View all results (1)" "option_role" exists
And I click on "Clear search input" "button" in the ".user-search" "css_element"
And "View all results (1)" "option_role" should not be visible
Scenario: A teacher can search to find specified users
# Case: Standard search.
Given I set the field "Search users" to "Dummy User"
And I wait until "View all results (1)" "option_role" exists
When I click on "Dummy User" "list_item"
Then the following should exist in the "generaltable" table:
| -1- |
| Dummy User |
And the following should not exist in the "generaltable" table:
| -1- |
| Teacher 1 |
| Student 1 |
| User Example |
| User Test |
| Turtle Manatee |
# Case: No users found.
When I set the field "Search users" to "Plagiarism"
And I should see "No results for \"Plagiarism\""
# Table remains unchanged as the user had no results to select from the dropdown.
And the following should exist in the "generaltable" table:
| -1- |
| Dummy User |
And the following should not exist in the "generaltable" table:
| -1- |
| Teacher 1 |
| Student 1 |
| User Example |
| User Test |
| Turtle Manatee |
# Case: Multiple users found and select only one result.
Then I set the field "Search users" to "User"
And I wait until "View all results (3)" "option_role" exists
And I confirm "Dummy User" exists in the "Search users" search combo box
And I confirm "User Example" exists in the "Search users" search combo box
And I confirm "User Test" exists in the "Search users" search combo box
And I confirm "Turtle Manatee" does not exist in the "Search users" search combo box
# Check if the matched field names (by lines) includes some identifiable info to help differentiate similar users.
And I confirm "User (student2@example.com)" exists in the "Search users" search combo box
And I confirm "User (student3@example.com)" exists in the "Search users" search combo box
And I confirm "User (student4@example.com)" exists in the "Search users" search combo box
And I click on "Dummy User" "list_item"
And I wait until the page is ready
And the following should exist in the "generaltable" table:
| -1- |
| Dummy User |
And the following should not exist in the "generaltable" table:
| -1- |
| Teacher 1 |
| Student 1 |
| User Example |
| User Test |
| Turtle Manatee |
# Business case: When searching with multiple partial matches, show the matches in the dropdown + a "View all results for (Bob)"
# Business case cont. When pressing enter with multiple partial matches, behave like when you select the "View all results for (Bob)"
# Case: Multiple users found and select all partial matches.
And I set the field "Search users" to "User"
And I wait until "View all results (3)" "option_role" exists
# Dont need to check if all users are in the dropdown, we checked that earlier in this test.
And I click on "View all results (3)" "option_role"
And I wait until the page is ready
And the following should exist in the "generaltable" table:
| -1- |
| Dummy User |
| User Example |
| User Test |
And the following should not exist in the "generaltable" table:
| -1- |
| Teacher 1 |
| Student 1 |
| Turtle Manatee |
And I click on "Clear" "link" in the ".user-search" "css_element"
And I wait until the page is ready
And the following should exist in the "generaltable" table:
| -1- |
| Turtle Manatee |
| Student 1 |
| User Example |
| User Test |
| Dummy User |
Scenario: A teacher can quickly tell that a search is active on the current table
When I click on "Turtle" in the "Search users" search combo box
# The search input should contain the name of the user we have selected, so that it is clear that the result pertains to a specific user.
Then the field "Search users" matches value "Turtle Manatee"
# Test if we can then further retain the turtle result set and further filter from there.
And I set the field "Search users" to "Turtle plagiarism"
And "Turtle Manatee" "list_item" should not be visible
And I should see "No results for \"Turtle plagiarism\""
Scenario: A teacher can search for values besides the users' name
Given I set the field "Search users" to "student5@example.com"
And I wait until "View all results (1)" "option_role" exists
And "Turtle Manatee" "list_item" should exist
And I set the field "Search users" to "@example.com"
And I wait until "View all results (5)" "option_role" exists
# Note: All learners match this email & showing emails is current default.
And I confirm "Dummy User" exists in the "Search users" search combo box
And I confirm "User Example" exists in the "Search users" search combo box
And I confirm "User Test" exists in the "Search users" search combo box
And I confirm "Student 1" exists in the "Search users" search combo box
And I confirm "Turtle Manatee" exists in the "Search users" search combo box
# Search on the country field.
When I set the field "Search users" to "JP"
And I wait until "Turtle Manatee" "list_item" does not exist
And I confirm "Dummy User" exists in the "Search users" search combo box
And I confirm "User Test" exists in the "Search users" search combo box
# Search on the city field.
And I set the field "Search users" to "Hanoi"
And I wait until "User Test" "list_item" does not exist
And I confirm "Student 1" exists in the "Search users" search combo box
# Search on the institution field.
And I set the field "Search users" to "ABCD"
And I wait until "Dummy User" "list_item" exists
And I confirm "User Example" exists in the "Search users" search combo box
And I confirm "User Test" exists in the "Search users" search combo box
And I confirm "Student 1" exists in the "Search users" search combo box
And I confirm "Turtle Manatee" exists in the "Search users" search combo box
# Search on the department field.
And I set the field "Search users" to "ABC3"
And I wait until "User Example" "list_item" does not exist
And I confirm "User Test" exists in the "Search users" search combo box
And I confirm "Turtle Manatee" exists in the "Search users" search combo box
# Search on the phone1 field.
And I set the field "Search users" to "4365899871"
And I wait until "User Test" "list_item" does not exist
And I confirm "Dummy User" exists in the "Search users" search combo box
# Search on the phone2 field.
And I set the field "Search users" to "2149871323"
And I wait until "Dummy User" "list_item" does not exist
And I confirm "User Test" exists in the "Search users" search combo box
# Search on the institution field then press enter to show the record set.
And I set the field "Search users" to "ABC"
And I wait until "Turtle Manatee" "list_item" exists
And I confirm "Dummy User" exists in the "Search users" search combo box
And I confirm "User Example" exists in the "Search users" search combo box
And I confirm "User Test" exists in the "Search users" search combo box
And I confirm "Student 1" exists in the "Search users" search combo box
And I press the down key
And I press the enter key
And I wait "1" seconds
And the following should exist in the "generaltable" table:
| -1- |
| Student 1 |
And the following should not exist in the "generaltable" table:
| -1- |
| User Example |
| User Test |
| Dummy User |
| Turtle Manatee |
| Teacher 1 |
Scenario: A teacher can set focus and search using the input are with a keyboard
Given I set the field "Search users" to "ABC"
And the focused element is "Search users" "field"
And I wait until "Turtle Manatee" "option_role" exists
# Basic tests for the page.
When I press the down key
And ".active" "css_element" should exist in the "Student 1" "option_role"
And I press the up key
And ".active" "css_element" should exist in the "View all results (5)" "option_role"
And I press the down key
And ".active" "css_element" should exist in the "Student 1" "option_role"
And I press the escape key
And the focused element is "Search users" "field"
Then I set the field "Search users" to "Goodmeme"
And I press the down key
And the focused element is "Search users" "field"
And I set the field "Search users" to "ABC"
And I wait until "Turtle Manatee" "option_role" exists
And I press the down key
And ".active" "css_element" should exist in the "Student 1" "option_role"
# Lets check the tabbing order.
And I set the field "Search users" to "ABC"
And I click on "Search users" "field"
And I wait until "Turtle Manatee" "option_role" exists
And I press the tab key
And the focused element is "Clear search input" "button"
And I press the tab key
And ".groupsearchwidget" "css_element" should exist
# Ensure we can interact with the input & clear search options with the keyboard.
# Space & Enter have the same handling for triggering the two functionalities.
And I set the field "Search users" to "User"
And I press the up key
And I press the enter key
And I wait to be redirected
And the following should exist in the "generaltable" table:
| -1- |
| Dummy User |
| User Example |
| User Test |
And the following should not exist in the "generaltable" table:
| -1- |
| Teacher 1 |
| Student 1 |
| Turtle Manatee |
Scenario: Once a teacher searches, it'll apply the currently set filters and inform the teacher as such
# Set up a basic filtering case.
Given I click on "Advanced group" in the "Search groups" search combo box
And the following should exist in the "generaltable" table:
| -1- |
| User Example |
And the following should not exist in the "generaltable" table:
| -1- |
| Teacher 1 |
| Student 1 |
| User Test |
| Dummy User |
| Turtle Manatee |
# Begin the search checking if we are adhering the filters.
When I set the field "Search users" to "Turtle"
Then I confirm "Turtle Manatee" does not exist in the "Search users" search combo box
Scenario: As a teacher I can dynamically find users whilst ignoring pagination
Given "11" "users" exist with the following data:
| username | students[count] |
| firstname | Student |
| lastname | s[count] |
| email | students[count]@example.com |
And "11" "course enrolments" exist with the following data:
| user | students[count] |
| course | C1 |
| role |student |
And I reload the page
And the field "perpage" matches value "10"
And the following should not exist in the "generaltable" table:
| -1- |
| Student s11 |
When I set the field "Search users" to "11"
# One of the users' phone numbers also matches.
And I wait until "View all results (1)" "option_role" exists
Then I confirm "Student s11" exists in the "Search users" search combo box

View file

@ -40,6 +40,7 @@
<span class="d-none" data-region="courseid" data-courseid="{{courseid}}"></span>
<span class="d-none" data-region="groupid" data-groupid="{{group}}"></span>
<span class="d-none" data-region="instance" data-instance="{{instance}}"></span>
<span class="d-none" data-region="currentvalue" data-currentvalue="{{currentvalue}}"></span>
{{< core/search_input_auto }}
{{$label}}{{#str}}searchusers, core{{/str}}{{/label}}
{{$placeholder}}{{#str}}searchusers, core{{/str}}{{/placeholder}}