diff --git a/grade/lib.php b/grade/lib.php index 7c3dee9b26468..23607f0f688fc 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -1051,6 +1051,12 @@ class grade_plugin_return { * @var int */ public $page; + /** + * Search string + * + * @var string + */ + public $search; /** * Constructor @@ -1064,6 +1070,7 @@ public function __construct($params = []) { $this->userid = optional_param('gpr_userid', null, PARAM_INT); $this->groupid = optional_param('gpr_groupid', null, PARAM_INT); $this->page = optional_param('gpr_page', null, PARAM_INT); + $this->search = optional_param('gpr_search', '', PARAM_NOTAGS); foreach ($params as $key => $value) { if (property_exists($this, $key)) { @@ -1217,6 +1224,12 @@ public function get_form_fields() { if (!empty($this->page)) { $result .= ''; } + + if (!empty($this->search)) { + $result .= html_writer::empty_tag('input', + ['type' => 'hidden', 'name' => 'gpr_search', 'value' => $this->search]); + } + return $result; } diff --git a/grade/report/grader/amd/build/search.min.js b/grade/report/grader/amd/build/search.min.js index 27e511526462a..f9d3a26153dca 100644 --- a/grade/report/grader/amd/build/search.min.js +++ b/grade/report/grader/amd/build/search.min.js @@ -1,3 +1,3 @@ -define("gradereport_grader/search",["exports","gradereport_grader/search/search_class","gradereport_grader/search/repository","core/str","core/url","core/templates"],(function(_exports,_search_class,Repository,_str,_url,_templates){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)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_class=_interopRequireDefault(_search_class),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),_url=_interopRequireDefault(_url);const selectors_component=".user-search",selectors_courseid='[data-region="courseid"]',selectors_resetPageButton='[data-action="resetpage"]',courseID=document.querySelector(selectors_component).querySelector(selectors_courseid).dataset.courseid,bannedFilterFields=["profileimageurlsmall","profileimageurl","id","link","matchingField","matchingFieldName"];class UserSearch extends _search_class.default{constructor(){var obj,key,value;super(),value=null,(key="profilestringmap")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}static init(){return new UserSearch}setComponentSelector(){return".user-search"}setDropdownSelector(){return".usersearchdropdown"}setTriggerSelector(){return".usersearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/search/resultset",{users:this.getMatchedResults().slice(0,5),hasusers:this.getMatchedResults().length>0,matches:this.getMatchedResults().length,showing:this.getMatchedResults().slice(0,5).length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchDataset(){return Repository.userFetch(courseID).then((r=>r.users))}async filterDataset(filterableData){return filterableData.filter((user=>Object.keys(user).some((key=>""!==user[key]&&!bannedFilterFields.includes(key)&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){var _stringMap$get;const valueString=value.toString().toLowerCase();if(valueString.includes(this.getPreppedSearchTerm())){user.matchingFieldName=null!==(_stringMap$get=stringMap.get(key))&&void 0!==_stringMap$get?_stringMap$get:key,user.matchingField=valueString.replace(this.getPreppedSearchTerm(),''.concat(this.getSearchTerm(),"")),user.matchingField="".concat(user.matchingField," (").concat(user.email,")"),user.link=this.selectOneLink(user.id);break}}return user})))}clickHandler(e){super.clickHandler(e),e.target===this.getHTMLElements().currentViewAll&&0===e.button&&(window.location=this.selectAllResultsLink()),e.target.closest(selectors_resetPageButton)&&(window.location=e.target.closest(selectors_resetPageButton).href)}keyHandler(e){switch(super.keyHandler(e),e.target!==this.getHTMLElements().currentViewAll||"Enter"!==e.key&&"Space"!==e.key||(window.location=this.selectAllResultsLink()),e.key){case"Enter":case" ":if(document.activeElement===this.getHTMLElements().searchInput){if(" "===e.key)break;window.location=this.selectAllResultsLink();break}if(document.activeElement===this.getHTMLElements().clearSearchButton){this.closeSearch(!0);break}if(e.target.closest(selectors_resetPageButton)){window.location=e.target.closest(selectors_resetPageButton).href;break}if(e.target.closest(".dropdown-item")){e.preventDefault(),window.location=e.target.closest(".dropdown-item").href;break}break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0});break;case"Tab":e.target.closest(this.selectors.clearSearch)&&(this.currentViewAll&&!e.shiftKey?(e.preventDefault(),this.currentViewAll.focus({preventScroll:!0})):this.closeSearch())}}selectAllResultsLink(){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,searchvalue:this.getSearchTerm()},!1)}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,searchvalue:this.getSearchTerm(),userid:userID},!1)}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.get_strings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default})); +define("gradereport_grader/search",["exports","gradereport_grader/search/search_class","gradereport_grader/search/repository","core/str","core/url","core/templates"],(function(_exports,_search_class,Repository,_str,_url,_templates){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)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_search_class=_interopRequireDefault(_search_class),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),_url=_interopRequireDefault(_url);const selectors_component=".user-search",selectors_courseid='[data-region="courseid"]',selectors_resetPageButton='[data-action="resetpage"]',courseID=document.querySelector(selectors_component).querySelector(selectors_courseid).dataset.courseid,bannedFilterFields=["profileimageurlsmall","profileimageurl","id","link","matchingField","matchingFieldName"];class UserSearch extends _search_class.default{constructor(){var obj,key,value;super(),value=null,(key="profilestringmap")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value}static init(){return new UserSearch}setComponentSelector(){return".user-search"}setDropdownSelector(){return".usersearchdropdown"}setTriggerSelector(){return".usersearchwidget"}async renderDropdown(){const{html:html,js:js}=await(0,_templates.renderForPromise)("gradereport_grader/search/resultset",{users:this.getMatchedResults().slice(0,5),hasusers:this.getMatchedResults().length>0,matches:this.getMatchedResults().length,showing:this.getMatchedResults().slice(0,5).length,searchterm:this.getSearchTerm(),selectall:this.selectAllResultsLink()});(0,_templates.replaceNodeContents)(this.getHTMLElements().searchDropdown,html,js)}fetchDataset(){return Repository.userFetch(courseID).then((r=>r.users))}async filterDataset(filterableData){return filterableData.filter((user=>Object.keys(user).some((key=>""!==user[key]&&!bannedFilterFields.includes(key)&&user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm())))))}async filterMatchDataset(){const stringMap=await this.getStringMap();this.setMatchedResults(this.getMatchedResults().map((user=>{for(const[key,value]of Object.entries(user)){var _stringMap$get;const valueString=value.toString().toLowerCase();if(valueString.includes(this.getPreppedSearchTerm())){user.matchingFieldName=null!==(_stringMap$get=stringMap.get(key))&&void 0!==_stringMap$get?_stringMap$get:key,user.matchingField=valueString.replace(this.getPreppedSearchTerm(),''.concat(this.getSearchTerm(),"")),user.matchingField="".concat(user.matchingField," (").concat(user.email,")"),user.link=this.selectOneLink(user.id);break}}return user})))}clickHandler(e){super.clickHandler(e),e.target===this.getHTMLElements().currentViewAll&&0===e.button&&(window.location=this.selectAllResultsLink()),e.target.closest(selectors_resetPageButton)&&(window.location=e.target.closest(selectors_resetPageButton).href)}keyHandler(e){switch(super.keyHandler(e),e.target!==this.getHTMLElements().currentViewAll||"Enter"!==e.key&&"Space"!==e.key||(window.location=this.selectAllResultsLink()),e.key){case"Enter":case" ":if(document.activeElement===this.getHTMLElements().searchInput){if(" "===e.key)break;window.location=this.selectAllResultsLink();break}if(document.activeElement===this.getHTMLElements().clearSearchButton){this.closeSearch(!0);break}if(e.target.closest(selectors_resetPageButton)){window.location=e.target.closest(selectors_resetPageButton).href;break}if(e.target.closest(".dropdown-item")){e.preventDefault(),window.location=e.target.closest(".dropdown-item").href;break}break;case"Escape":this.toggleDropdown(),this.searchInput.focus({preventScroll:!0});break;case"Tab":e.target.closest(this.selectors.clearSearch)&&(this.currentViewAll&&!e.shiftKey?(e.preventDefault(),this.currentViewAll.focus({preventScroll:!0})):this.closeSearch())}}selectAllResultsLink(){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,gpr_search:this.getSearchTerm()},!1)}selectOneLink(userID){return _url.default.relativeUrl("/grade/report/grader/index.php",{id:courseID,gpr_search:this.getSearchTerm(),gpr_userid:userID},!1)}getStringMap(){if(!this.profilestringmap){const requiredStrings=["username","firstname","lastname","email","city","country","department","institution","idnumber","phone1","phone2"];this.profilestringmap=(0,_str.get_strings)(requiredStrings.map((key=>({key:key})))).then((stringArray=>new Map(requiredStrings.map(((key,index)=>[key,stringArray[index]])))))}return this.profilestringmap}}return _exports.default=UserSearch,_exports.default})); //# sourceMappingURL=search.min.js.map \ No newline at end of file diff --git a/grade/report/grader/amd/build/search.min.js.map b/grade/report/grader/amd/build/search.min.js.map index 98a480505b151..e8f168ba14590 100644 --- a/grade/report/grader/amd/build/search.min.js.map +++ b/grade/report/grader/amd/build/search.min.js.map @@ -1 +1 @@ -{"version":3,"file":"search.min.js","sources":["../src/search.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 .\n\n/**\n * Allow the user to search for learners within the grader report.\n * Have to basically search twice on the dataset to avoid passing around massive csv params whilst allowing debouncing.\n *\n * @module gradereport_grader/search\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GradebookSearchClass from 'gradereport_grader/search/search_class';\nimport * as Repository from 'gradereport_grader/search/repository';\nimport {get_strings as getStrings} from 'core/str';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\nconst bannedFilterFields = ['profileimageurlsmall', 'profileimageurl', 'id', 'link', 'matchingField', 'matchingFieldName'];\n\nexport default class UserSearch extends GradebookSearchClass {\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n setComponentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n setDropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n setTriggerSelector() {\n return '.usersearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('gradereport_grader/search/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasusers: this.getMatchedResults().length > 0,\n matches: this.getMatchedResults().length,\n showing: this.getMatchedResults().slice(0, 5).length,\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\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(courseID).then((r) => r.users);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || bannedFilterFields.includes(key)) {\n return false;\n }\n return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n *\n * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n const valueString = value.toString().toLowerCase();\n if (!valueString.includes(this.getPreppedSearchTerm())) {\n continue;\n }\n // Ensure we have a good string, otherwise fallback to the key.\n user.matchingFieldName = stringMap.get(key) ?? key;\n user.matchingField = valueString.replace(\n this.getPreppedSearchTerm(),\n `${this.getSearchTerm()}`\n );\n user.matchingField = `${user.matchingField} (${user.email})`;\n user.link = this.selectOneLink(user.id);\n break;\n }\n return user;\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n clickHandler(e) {\n super.clickHandler(e);\n if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {\n window.location = this.selectAllResultsLink();\n }\n if (e.target.closest(selectors.resetPageButton)) {\n window.location = e.target.closest(selectors.resetPageButton).href;\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n super.keyHandler(e);\n\n if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) {\n window.location = this.selectAllResultsLink();\n }\n\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'Enter':\n case ' ':\n if (document.activeElement === this.getHTMLElements().searchInput) {\n if (e.key === ' ') {\n break;\n } else {\n window.location = this.selectAllResultsLink();\n break;\n }\n }\n if (document.activeElement === this.getHTMLElements().clearSearchButton) {\n this.closeSearch(true);\n break;\n }\n if (e.target.closest(selectors.resetPageButton)) {\n window.location = e.target.closest(selectors.resetPageButton).href;\n break;\n }\n if (e.target.closest('.dropdown-item')) {\n e.preventDefault();\n window.location = e.target.closest('.dropdown-item').href;\n break;\n }\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n case 'Tab':\n // If the current focus is on clear search, then check if viewall exists then around tab to it.\n if (e.target.closest(this.selectors.clearSearch)) {\n if (this.currentViewAll && !e.shiftKey) {\n e.preventDefault();\n this.currentViewAll.focus({preventScroll: true});\n } else {\n this.closeSearch();\n }\n }\n break;\n }\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n searchvalue: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the view all 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 return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n searchvalue: this.getSearchTerm(),\n userid: userID,\n }, false);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n getStringMap() {\n if (!this.profilestringmap) {\n const requiredStrings = [\n 'username',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.profilestringmap;\n }\n}\n"],"names":["selectors","courseID","document","querySelector","dataset","courseid","bannedFilterFields","UserSearch","GradebookSearchClass","constructor","setComponentSelector","setDropdownSelector","setTriggerSelector","html","js","users","this","getMatchedResults","slice","hasusers","length","matches","showing","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchDropdown","fetchDataset","Repository","userFetch","then","r","filterableData","filter","user","Object","keys","some","key","includes","toString","toLowerCase","getPreppedSearchTerm","stringMap","getStringMap","setMatchedResults","map","value","entries","valueString","matchingFieldName","get","matchingField","replace","email","link","selectOneLink","id","clickHandler","e","target","currentViewAll","button","window","location","closest","href","keyHandler","activeElement","searchInput","clearSearchButton","closeSearch","preventDefault","toggleDropdown","focus","preventScroll","clearSearch","shiftKey","Url","relativeUrl","searchvalue","userID","userid","profilestringmap","requiredStrings","stringArray","Map","index"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"search.min.js","sources":["../src/search.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 .\n\n/**\n * Allow the user to search for learners within the grader report.\n * Have to basically search twice on the dataset to avoid passing around massive csv params whilst allowing debouncing.\n *\n * @module gradereport_grader/search\n * @copyright 2023 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport GradebookSearchClass from 'gradereport_grader/search/search_class';\nimport * as Repository from 'gradereport_grader/search/repository';\nimport {get_strings as getStrings} from 'core/str';\nimport Url from 'core/url';\nimport {renderForPromise, replaceNodeContents} from 'core/templates';\n\n// Define our standard lookups.\nconst selectors = {\n component: '.user-search',\n courseid: '[data-region=\"courseid\"]',\n resetPageButton: '[data-action=\"resetpage\"]',\n};\nconst component = document.querySelector(selectors.component);\nconst courseID = component.querySelector(selectors.courseid).dataset.courseid;\nconst bannedFilterFields = ['profileimageurlsmall', 'profileimageurl', 'id', 'link', 'matchingField', 'matchingFieldName'];\n\nexport default class UserSearch extends GradebookSearchClass {\n\n // A map of user profile field names that is human-readable.\n profilestringmap = null;\n\n constructor() {\n super();\n }\n\n static init() {\n return new UserSearch();\n }\n\n /**\n * The overall div that contains the searching widget.\n *\n * @returns {string}\n */\n setComponentSelector() {\n return '.user-search';\n }\n\n /**\n * The dropdown div that contains the searching widget result space.\n *\n * @returns {string}\n */\n setDropdownSelector() {\n return '.usersearchdropdown';\n }\n\n /**\n * The triggering div that contains the searching widget.\n *\n * @returns {string}\n */\n setTriggerSelector() {\n return '.usersearchwidget';\n }\n\n /**\n * Build the content then replace the node.\n */\n async renderDropdown() {\n const {html, js} = await renderForPromise('gradereport_grader/search/resultset', {\n users: this.getMatchedResults().slice(0, 5),\n hasusers: this.getMatchedResults().length > 0,\n matches: this.getMatchedResults().length,\n showing: this.getMatchedResults().slice(0, 5).length,\n searchterm: this.getSearchTerm(),\n selectall: this.selectAllResultsLink(),\n });\n replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);\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(courseID).then((r) => r.users);\n }\n\n /**\n * Dictate to the search component how and what we want to match upon.\n *\n * @param {Array} filterableData\n * @returns {Array} The users that match the given criteria.\n */\n async filterDataset(filterableData) {\n return filterableData.filter((user) => Object.keys(user).some((key) => {\n if (user[key] === \"\" || bannedFilterFields.includes(key)) {\n return false;\n }\n return user[key].toString().toLowerCase().includes(this.getPreppedSearchTerm());\n }));\n }\n\n /**\n * Given we have a subset of the dataset, set the field that we matched upon to inform the end user.\n *\n * @returns {Array} The results with the matched fields inserted.\n */\n async filterMatchDataset() {\n const stringMap = await this.getStringMap();\n this.setMatchedResults(\n this.getMatchedResults().map((user) => {\n for (const [key, value] of Object.entries(user)) {\n const valueString = value.toString().toLowerCase();\n if (!valueString.includes(this.getPreppedSearchTerm())) {\n continue;\n }\n // Ensure we have a good string, otherwise fallback to the key.\n user.matchingFieldName = stringMap.get(key) ?? key;\n user.matchingField = valueString.replace(\n this.getPreppedSearchTerm(),\n `${this.getSearchTerm()}`\n );\n user.matchingField = `${user.matchingField} (${user.email})`;\n user.link = this.selectOneLink(user.id);\n break;\n }\n return user;\n })\n );\n }\n\n /**\n * The handler for when a user interacts with the component.\n *\n * @param {MouseEvent} e The triggering event that we are working with.\n */\n clickHandler(e) {\n super.clickHandler(e);\n if (e.target === this.getHTMLElements().currentViewAll && e.button === 0) {\n window.location = this.selectAllResultsLink();\n }\n if (e.target.closest(selectors.resetPageButton)) {\n window.location = e.target.closest(selectors.resetPageButton).href;\n }\n }\n\n /**\n * The handler for when a user presses a key within the component.\n *\n * @param {KeyboardEvent} e The triggering event that we are working with.\n */\n keyHandler(e) {\n super.keyHandler(e);\n\n if (e.target === this.getHTMLElements().currentViewAll && (e.key === 'Enter' || e.key === 'Space')) {\n window.location = this.selectAllResultsLink();\n }\n\n // Switch the key presses to handle keyboard nav.\n switch (e.key) {\n case 'Enter':\n case ' ':\n if (document.activeElement === this.getHTMLElements().searchInput) {\n if (e.key === ' ') {\n break;\n } else {\n window.location = this.selectAllResultsLink();\n break;\n }\n }\n if (document.activeElement === this.getHTMLElements().clearSearchButton) {\n this.closeSearch(true);\n break;\n }\n if (e.target.closest(selectors.resetPageButton)) {\n window.location = e.target.closest(selectors.resetPageButton).href;\n break;\n }\n if (e.target.closest('.dropdown-item')) {\n e.preventDefault();\n window.location = e.target.closest('.dropdown-item').href;\n break;\n }\n break;\n case 'Escape':\n this.toggleDropdown();\n this.searchInput.focus({preventScroll: true});\n break;\n case 'Tab':\n // If the current focus is on clear search, then check if viewall exists then around tab to it.\n if (e.target.closest(this.selectors.clearSearch)) {\n if (this.currentViewAll && !e.shiftKey) {\n e.preventDefault();\n this.currentViewAll.focus({preventScroll: true});\n } else {\n this.closeSearch();\n }\n }\n break;\n }\n }\n\n /**\n * Build up the view all link.\n *\n * @returns {string|*}\n */\n selectAllResultsLink() {\n return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm()\n }, false);\n }\n\n /**\n * Build up the view all 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 return Url.relativeUrl('/grade/report/grader/index.php', {\n id: courseID,\n gpr_search: this.getSearchTerm(),\n gpr_userid: userID,\n }, false);\n }\n\n /**\n * Given the set of profile fields we can possibly search, fetch their strings,\n * so we can report to screen readers the field that matched.\n *\n * @returns {Promise}\n */\n getStringMap() {\n if (!this.profilestringmap) {\n const requiredStrings = [\n 'username',\n 'firstname',\n 'lastname',\n 'email',\n 'city',\n 'country',\n 'department',\n 'institution',\n 'idnumber',\n 'phone1',\n 'phone2',\n ];\n this.profilestringmap = getStrings(requiredStrings.map((key) => ({key})))\n .then((stringArray) => new Map(\n requiredStrings.map((key, index) => ([key, stringArray[index]]))\n ));\n }\n return this.profilestringmap;\n }\n}\n"],"names":["selectors","courseID","document","querySelector","dataset","courseid","bannedFilterFields","UserSearch","GradebookSearchClass","constructor","setComponentSelector","setDropdownSelector","setTriggerSelector","html","js","users","this","getMatchedResults","slice","hasusers","length","matches","showing","searchterm","getSearchTerm","selectall","selectAllResultsLink","getHTMLElements","searchDropdown","fetchDataset","Repository","userFetch","then","r","filterableData","filter","user","Object","keys","some","key","includes","toString","toLowerCase","getPreppedSearchTerm","stringMap","getStringMap","setMatchedResults","map","value","entries","valueString","matchingFieldName","get","matchingField","replace","email","link","selectOneLink","id","clickHandler","e","target","currentViewAll","button","window","location","closest","href","keyHandler","activeElement","searchInput","clearSearchButton","closeSearch","preventDefault","toggleDropdown","focus","preventScroll","clearSearch","shiftKey","Url","relativeUrl","gpr_search","userID","gpr_userid","profilestringmap","requiredStrings","stringArray","Map","index"],"mappings":"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"} \ No newline at end of file diff --git a/grade/report/grader/amd/src/search.js b/grade/report/grader/amd/src/search.js index d751598552826..3964c65e34113 100644 --- a/grade/report/grader/amd/src/search.js +++ b/grade/report/grader/amd/src/search.js @@ -224,7 +224,7 @@ export default class UserSearch extends GradebookSearchClass { selectAllResultsLink() { return Url.relativeUrl('/grade/report/grader/index.php', { id: courseID, - searchvalue: this.getSearchTerm() + gpr_search: this.getSearchTerm() }, false); } @@ -237,8 +237,8 @@ export default class UserSearch extends GradebookSearchClass { selectOneLink(userID) { return Url.relativeUrl('/grade/report/grader/index.php', { id: courseID, - searchvalue: this.getSearchTerm(), - userid: userID, + gpr_search: this.getSearchTerm(), + gpr_userid: userID, }, false); } diff --git a/grade/report/grader/classes/output/action_bar.php b/grade/report/grader/classes/output/action_bar.php index f6a367f301cbc..3c4254ae158a3 100644 --- a/grade/report/grader/classes/output/action_bar.php +++ b/grade/report/grader/classes/output/action_bar.php @@ -40,7 +40,7 @@ class action_bar extends \core_grades\output\action_bar { public function __construct(\context_course $context) { parent::__construct($context); - $this->usersearch = optional_param('searchvalue', '', PARAM_NOTAGS); + $this->usersearch = optional_param('gpr_search', '', PARAM_NOTAGS); } /** diff --git a/grade/report/grader/tests/behat/tertiary_navigation_searching.feature b/grade/report/grader/tests/behat/tertiary_navigation_searching.feature index cbef4b7ea5347..03179cab5cecb 100644 --- a/grade/report/grader/tests/behat/tertiary_navigation_searching.feature +++ b/grade/report/grader/tests/behat/tertiary_navigation_searching.feature @@ -340,3 +340,80 @@ Feature: Within the grader report, test that we can search for users # One of the users' phone numbers also matches. And I wait until "View all results (2)" "link" exists Then "Student s42" "list_item" should exist in the ".user-search" "css_element" + + Scenario: As a teacher I save grades using search and pagination + Given "42" "users" exist with the following data: + | username | students[count] | + | firstname | Student | + | lastname | test[count] | + | email | students[count]@example.com | + And "42" "course enrolments" exist with the following data: + | user | students[count] | + | course | C1 | + | role |student | + And I reload the page + And I turn editing mode on + And the field "perpage" matches value "20" + # Search for a single user on second page and save grades. + When I set the field "Search users" to "test42" + And I wait until "View all results (1)" "link" exists + And I press the enter key + And I wait until the page is ready + And I give the grade "80.00" to the user "Student test42" for the grade item "Test assignment one" + And I press "Save changes" + And I wait until the page is ready + Then the field "Search users" matches value "test42" + And the following should exist in the "user-grades" table: + | -1- | + | Student test42 | + And I set the field "Search users" to "test4" + And I click on "Student test41" "option_role" + And I wait until the page is ready + And I give the grade "70.00" to the user "Student test41" for the grade item "Test assignment one" + And I press "Save changes" + And I wait until the page is ready + Then the field "Search users" matches value "test4" + And the following should exist in the "user-grades" table: + | -1- | + | Student test41 | + And the following should exist in the "user-grades" table: + | -1- | + | Student test42 | + And I click on "Clear" "link" in the ".user-search" "css_element" + And I wait until the page is ready + And the following should not exist in the "user-grades" table: + | -1- | + | Student test42 | + And I click on "2" "link" in the ".stickyfooter .pagination" "css_element" + And I wait until the page is ready + And the following should exist in the "user-grades" table: + | -1- | + | Student test41 | + | Student test42 | + # Set grade for a single user on second page without search and save grades. + And I give the grade "70.00" to the user "Student test41" for the grade item "Test assignment one" + And I press "Save changes" + And I wait until the page is ready + # We are still on second page. + And the following should exist in the "user-grades" table: + | -1- | + | Student test41 | + | Student test42 | + # Search for multiple users on second page and save grades. + And I set the field "Search users" to "test4" + And I wait until "View all results (4)" "link" exists + And I press the enter key + And I wait until the page is ready + And I give the grade "10.00" to the user "Student test42" for the grade item "Test assignment one" + And I give the grade "20.00" to the user "Student test40" for the grade item "Test assignment one" + And I give the grade "30.00" to the user "Student test41" for the grade item "Test assignment one" + And I give the grade "40.00" to the user "Student test4" for the grade item "Test assignment one" + And I press "Save changes" + And I wait until the page is ready + Then the field "Search users" matches value "test4" + And the following should exist in the "user-grades" table: + | -1- | + | Student test4 | + | Student test40 | + | Student test41 | + | Student test42 | diff --git a/grade/report/lib.php b/grade/report/lib.php index 5d3d4aa43ca05..abe7dbeb83b89 100644 --- a/grade/report/lib.php +++ b/grade/report/lib.php @@ -214,8 +214,8 @@ public function __construct($courseid, $gpr, $context, $page=null) { // init gtree in child class // Set any url params. - $this->usersearch = optional_param('searchvalue', '', PARAM_NOTAGS); - $this->userid = optional_param('userid', -1, PARAM_INT); + $this->usersearch = optional_param('gpr_search', '', PARAM_NOTAGS); + $this->userid = optional_param('gpr_userid', -1, PARAM_INT); } /**