From e7c1c2841d02bfd7dd61033b3317ffb056e8eecf Mon Sep 17 00:00:00 2001 From: Mathew May Date: Fri, 16 Sep 2022 16:14:22 +0200 Subject: [PATCH] MDL-75362 gradereport_user: Roll in zero state work for user report --- .../amd/build/searchwidget/basewidget.min.js | 10 ++ .../build/searchwidget/basewidget.min.js.map | 1 + .../amd/build/searchwidget/repository.min.js | 10 ++ .../build/searchwidget/repository.min.js.map | 1 + grade/amd/src/searchwidget/basewidget.js | 164 ++++++++++++++++++ grade/amd/src/searchwidget/repository.js | 61 +++++++ .../get_enrolled_users_for_search_widget.php | 157 +++++++++++++++++ grade/lib.php | 2 +- grade/report/user/amd/build/user.min.js | 10 ++ grade/report/user/amd/build/user.min.js.map | 1 + grade/report/user/amd/src/user.js | 130 ++++++++++++++ grade/report/user/classes/external/user.php | 1 + grade/report/user/classes/report/user.php | 13 ++ grade/report/user/db/services.php | 2 +- grade/report/user/index.php | 47 +++-- .../report/user/lang/en/gradereport_user.php | 5 + grade/report/user/renderer.php | 4 +- .../report/user/templates/zero_state.mustache | 37 ++++ .../report/user/tests/behat/user_view.feature | 10 +- .../user/tests/behat/usersearch.feature | 33 ++++ .../user/tests/behat/view_usereport.feature | 2 +- grade/report/user/version.php | 2 +- grade/templates/searchwidget/error.mustache | 39 +++++ .../searchwidget/searchitem.mustache | 48 +++++ .../searchwidget/searchresults.mustache | 46 +++++ .../user/usersearch_body.mustache | 36 ++++ grade/tests/behat/behat_grade.php | 55 ++++++ grade/tests/behat/grade_aggregation.feature | 5 +- .../grade_calculated_grade_items.feature | 12 +- ...de_calculated_grade_items_20150627.feature | 12 +- .../behat/grade_calculated_weights.feature | 20 +-- ...ade_contribution_with_extra_credit.feature | 2 +- .../behat/grade_grade_minmax_change.feature | 4 +- grade/tests/behat/grade_hidden_items.feature | 2 +- ...grade_hidden_items_locked_category.feature | 2 +- grade/tests/behat/grade_mingrade.feature | 2 +- grade/tests/behat/grade_minmax.feature | 8 +- .../behat/grade_natural_exclude_empty.feature | 20 +-- ...ade_natural_exclude_empty_20150619.feature | 20 +-- grade/tests/behat/grade_scales.feature | 4 +- .../behat/grade_scales_aggregation.feature | 4 +- .../behat/grade_single_item_scales.feature | 4 +- grade/tests/behat/grade_view.feature | 2 +- lang/en/grades.php | 1 + lib/db/services.php | 7 + .../tests/behat/grading_attempts.feature | 20 +-- pix/f/clip-353 1.png | Bin 0 -> 29979 bytes theme/boost/scss/moodle/grade.scss | 19 ++ theme/boost/style/moodle.css | 13 ++ theme/classic/style/moodle.css | 13 ++ version.php | 2 +- 51 files changed, 1027 insertions(+), 98 deletions(-) create mode 100644 grade/amd/build/searchwidget/basewidget.min.js create mode 100644 grade/amd/build/searchwidget/basewidget.min.js.map create mode 100644 grade/amd/build/searchwidget/repository.min.js create mode 100644 grade/amd/build/searchwidget/repository.min.js.map create mode 100644 grade/amd/src/searchwidget/basewidget.js create mode 100644 grade/amd/src/searchwidget/repository.js create mode 100644 grade/classes/external/get_enrolled_users_for_search_widget.php create mode 100644 grade/report/user/amd/build/user.min.js create mode 100644 grade/report/user/amd/build/user.min.js.map create mode 100644 grade/report/user/amd/src/user.js create mode 100644 grade/report/user/templates/zero_state.mustache create mode 100644 grade/report/user/tests/behat/usersearch.feature create mode 100644 grade/templates/searchwidget/error.mustache create mode 100644 grade/templates/searchwidget/searchitem.mustache create mode 100644 grade/templates/searchwidget/searchresults.mustache create mode 100644 grade/templates/searchwidget/user/usersearch_body.mustache create mode 100644 pix/f/clip-353 1.png diff --git a/grade/amd/build/searchwidget/basewidget.min.js b/grade/amd/build/searchwidget/basewidget.min.js new file mode 100644 index 00000000000..213c859f0af --- /dev/null +++ b/grade/amd/build/searchwidget/basewidget.min.js @@ -0,0 +1,10 @@ +define("core_grades/searchwidget/basewidget",["exports","core/modal_factory","core/modal_events","core/utils","core/templates"],(function(_exports,ModalFactory,ModalEvents,_utils,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 _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +/** + * A small modal to search users or grade items within the gradebook. + * + * @module core_grades/searchwidget/basewidget + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.promisesAndResolvers=_exports.init=void 0,ModalFactory=_interopRequireWildcard(ModalFactory),ModalEvents=_interopRequireWildcard(ModalEvents),Templates=_interopRequireWildcard(Templates);_exports.init=function(bodyPromise,data,searchFunc,modalTitle){let unsearchableContent=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null;const modal=buildModal(bodyPromise,modalTitle);registerListenerEvents(modal,data,searchFunc,unsearchableContent)};const registerListenerEvents=(modal,data,searchFunc,unsearchableContent)=>{modal.then((modal=>{modal.getRoot().on(ModalEvents.hidden,(()=>{modal.destroy()})),modal.getBodyPromise().then((body=>body[0])).then((body=>{const searchInput=body.querySelector('input[data-action="search"]'),searchResultsContainer=body.querySelector('[data-region="search-results-container-widget"]');if(renderSearchResults(searchResultsContainer,data),unsearchableContent){body.querySelector('[data-region="unsearchable-content-container-widget"]').innerHTML+=unsearchableContent}return searchInput.addEventListener("input",(0,_utils.debounce)((()=>{renderSearchResults(searchResultsContainer,debounceCallee(searchInput.value,data,searchFunc()))}),300)),body})).catch()})).catch()},buildModal=(bodyPromise,modalTitle)=>ModalFactory.create({type:ModalFactory.types.DEFAULT,title:modalTitle,body:bodyPromise,small:!0,scrollable:!1,templateContext:{classes:"reportdatasearch modal-sm"}}).then((modal=>(modal.show(),modal))),debounceCallee=(searchValue,data,searchFunction)=>searchValue.length>0?searchFunction(data,searchValue):data,renderSearchResults=async(searchResultsContainer,searchResultsData)=>{const templateData={searchresults:searchResultsData},{html:html,js:js}=await Templates.renderForPromise("core_grades/searchwidget/searchresults",templateData);await Templates.replaceNodeContents(searchResultsContainer,html,js)};_exports.promisesAndResolvers=()=>{let bodyPromiseResolver;const bodyPromise=new Promise((resolve=>{bodyPromiseResolver=resolve}));return{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}}})); + +//# sourceMappingURL=basewidget.min.js.map \ No newline at end of file diff --git a/grade/amd/build/searchwidget/basewidget.min.js.map b/grade/amd/build/searchwidget/basewidget.min.js.map new file mode 100644 index 00000000000..ea796926cb4 --- /dev/null +++ b/grade/amd/build/searchwidget/basewidget.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"basewidget.min.js","sources":["../../src/searchwidget/basewidget.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 * A small modal to search users or grade items within the gradebook.\n *\n * @module core_grades/searchwidget/basewidget\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as ModalFactory from 'core/modal_factory';\nimport * as ModalEvents from 'core/modal_events';\nimport {debounce} from 'core/utils';\nimport * as Templates from 'core/templates';\n\n/**\n * Build the base searching widget.\n *\n * @method init\n * @param {Promise} bodyPromise The promise from the callee of the contents to place in the modal body.\n * @param {Array} data An array of all the data generated by the callee.\n * @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.\n * @param {string} modalTitle The name of the search widget.\n * @param {string|null} unsearchableContent The content rendered in a non-searchable area.\n */\nexport const init = (bodyPromise, data, searchFunc, modalTitle, unsearchableContent = null) => {\n const modal = buildModal(bodyPromise, modalTitle);\n registerListenerEvents(modal, data, searchFunc, unsearchableContent);\n};\n\n/**\n * Register chooser related event listeners.\n *\n * @method registerListenerEvents\n * @param {Promise} modal Our modal that we are working with.\n * @param {Array} data An array of all the data generated by the callee.\n * @param {Function} searchFunc Partially applied function we need to manage search the passed dataset.\n * @param {string|null} unsearchableContent The content rendered in a non-searchable area.\n */\nconst registerListenerEvents = (modal, data, searchFunc, unsearchableContent) => {\n modal.then(modal => {\n // We want to destroy this when the dialog is closed.\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n // Once the body of the modal has been resolved, add more features.\n modal.getBodyPromise()\n // The return value of getBodyPromise is a jquery object containing the body NodeElement.\n .then(body => body[0])\n .then(body => {\n const searchInput = body.querySelector('input[data-action=\"search\"]');\n const searchResultsContainer = body.querySelector('[data-region=\"search-results-container-widget\"]');\n\n renderSearchResults(searchResultsContainer, data);\n\n if (unsearchableContent) {\n const unsearchableContentContainer = body.querySelector(\n '[data-region=\"unsearchable-content-container-widget\"]');\n unsearchableContentContainer.innerHTML += unsearchableContent;\n }\n\n // The search input is triggered.\n searchInput.addEventListener('input', debounce(() => {\n // Display the search results.\n renderSearchResults(\n searchResultsContainer,\n debounceCallee(\n searchInput.value,\n data,\n searchFunc()\n )\n );\n }, 300));\n return body;\n }).catch();\n }).catch();\n};\n\n/**\n * Given an object we want to build a modal ready to show.\n *\n * @method buildModal\n * @param {Promise} bodyPromise Body promise that the caller should resolve.\n * @param {string} modalTitle The name of the search widget.\n * @return {Object} The modal ready to display immediately and render body in later.\n */\nconst buildModal = (bodyPromise, modalTitle) => {\n return ModalFactory.create({\n type: ModalFactory.types.DEFAULT,\n // TODO: Make this defined by the interface.\n title: modalTitle,\n body: bodyPromise,\n small: true,\n scrollable: false,\n templateContext: {\n classes: 'reportdatasearch modal-sm'\n }\n }).then(modal => {\n modal.show();\n return modal;\n });\n};\n\n/**\n * We have a small helper that'll call the curried search function allowing callers to filter\n * the data set however we want rather than defining how data must be filtered.\n *\n * @method debounceCallee\n * @param {String} searchValue The input from the user that we'll search against.\n * @param {Array} data An array of all the data generated by the callee.\n * @param {Function} searchFunction Partially applied function we need to manage search the passed dataset.\n * @return {Array} The filtered subset of the provided data that we'll then render into the results.\n */\nconst debounceCallee = (searchValue, data, searchFunction) => {\n if (searchValue.length > 0) { // Search query is present.\n return searchFunction(data, searchValue);\n }\n return data;\n};\n\n/**\n * Given the output of the callers' search function, render out the results into the modal.\n *\n * @method renderSearchResults\n * @param {HTMLElement} searchResultsContainer The DOM node of the widget where we'll render the provided results.\n * @param {Array} searchResultsData The filtered subset of the provided data that we'll then render into the results.\n */\nconst renderSearchResults = async(searchResultsContainer, searchResultsData) => {\n const templateData = {\n 'searchresults': searchResultsData,\n };\n // Build up the html & js ready to place into the help section.\n const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/searchresults', templateData);\n await Templates.replaceNodeContents(searchResultsContainer, html, js);\n};\n\n/**\n * We want to create the basic promises and hooks that the caller will implement, so we can build modals\n * ahead of time and allow the caller to resolve their promises once complete.\n *\n * @method promisesAndResolvers\n * @returns {{bodyPromise: Promise, bodyPromiseResolver}}\n */\nexport const promisesAndResolvers = () => {\n // We want to show the modal instantly but loading whilst waiting for our data.\n let bodyPromiseResolver;\n const bodyPromise = new Promise(resolve => {\n bodyPromiseResolver = resolve;\n });\n\n return {bodyPromiseResolver, bodyPromise};\n};\n"],"names":["bodyPromise","data","searchFunc","modalTitle","unsearchableContent","modal","buildModal","registerListenerEvents","then","getRoot","on","ModalEvents","hidden","destroy","getBodyPromise","body","searchInput","querySelector","searchResultsContainer","renderSearchResults","innerHTML","addEventListener","debounceCallee","value","catch","ModalFactory","create","type","types","DEFAULT","title","small","scrollable","templateContext","classes","show","searchValue","searchFunction","length","async","searchResultsData","templateData","html","js","Templates","renderForPromise","replaceNodeContents","bodyPromiseResolver","Promise","resolve"],"mappings":";;;;;;;+QAqCoB,SAACA,YAAaC,KAAMC,WAAYC,gBAAYC,2EAAsB,WAC5EC,MAAQC,WAAWN,YAAaG,YACtCI,uBAAuBF,MAAOJ,KAAMC,WAAYE,4BAY9CG,uBAAyB,CAACF,MAAOJ,KAAMC,WAAYE,uBACrDC,MAAMG,MAAKH,QAEPA,MAAMI,UAAUC,GAAGC,YAAYC,QAAQ,KACnCP,MAAMQ,aAGVR,MAAMS,iBAEDN,MAAKO,MAAQA,KAAK,KAClBP,MAAKO,aACIC,YAAcD,KAAKE,cAAc,+BACjCC,uBAAyBH,KAAKE,cAAc,sDAElDE,oBAAoBD,uBAAwBjB,MAExCG,oBAAqB,CACgBW,KAAKE,cACtC,yDACyBG,WAAahB,2BAI9CY,YAAYK,iBAAiB,SAAS,oBAAS,KAE3CF,oBACID,uBACAI,eACIN,YAAYO,MACZtB,KACAC,iBAGT,MACIa,QACRS,WACRA,SAWDlB,WAAa,CAACN,YAAaG,aACtBsB,aAAaC,OAAO,CACvBC,KAAMF,aAAaG,MAAMC,QAEzBC,MAAO3B,WACPY,KAAMf,YACN+B,OAAO,EACPC,YAAY,EACZC,gBAAiB,CACbC,QAAS,+BAEd1B,MAAKH,QACJA,MAAM8B,OACC9B,SAcTiB,eAAiB,CAACc,YAAanC,KAAMoC,iBACnCD,YAAYE,OAAS,EACdD,eAAepC,KAAMmC,aAEzBnC,KAULkB,oBAAsBoB,MAAMrB,uBAAwBsB,2BAChDC,aAAe,eACAD,oBAGfE,KAACA,KAADC,GAAOA,UAAYC,UAAUC,iBAAiB,yCAA0CJ,oBACxFG,UAAUE,oBAAoB5B,uBAAwBwB,KAAMC,mCAUlC,SAE5BI,0BACE/C,YAAc,IAAIgD,SAAQC,UAC5BF,oBAAsBE,iBAGnB,CAACF,oBAAAA,oBAAqB/C,YAAAA"} \ No newline at end of file diff --git a/grade/amd/build/searchwidget/repository.min.js b/grade/amd/build/searchwidget/repository.min.js new file mode 100644 index 00000000000..d57f4c6947d --- /dev/null +++ b/grade/amd/build/searchwidget/repository.min.js @@ -0,0 +1,10 @@ +define("core_grades/searchwidget/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj; +/** + * A repo for the search widget. + * + * @module core_grades/searchwidget/repository + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.userFetch=_exports.gradeitemFetch=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.userFetch=(courseid,actionBaseUrl)=>{const request={methodname:"core_grades_get_enrolled_users_for_search_widget",args:{courseid:courseid,actionbaseurl:actionBaseUrl}};return _ajax.default.call([request])[0]};_exports.gradeitemFetch=courseid=>{const request={methodname:"gradereport_singleview_get_grade_items_for_search_widget",args:{courseid:courseid}};return _ajax.default.call([request])[0]}})); + +//# sourceMappingURL=repository.min.js.map \ No newline at end of file diff --git a/grade/amd/build/searchwidget/repository.min.js.map b/grade/amd/build/searchwidget/repository.min.js.map new file mode 100644 index 00000000000..56978025869 --- /dev/null +++ b/grade/amd/build/searchwidget/repository.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"repository.min.js","sources":["../../src/searchwidget/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 .\n\n/**\n * A repo for the search widget.\n *\n * @module core_grades/searchwidget/repository\n * @copyright 2022 Mathew May \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 enrolled learners, so we may fetch their reports.\n *\n * @method userFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @param {string} actionBaseUrl The base URL for the user option.\n * @return {object} jQuery promise\n */\nexport const userFetch = (courseid, actionBaseUrl) => {\n const request = {\n methodname: 'core_grades_get_enrolled_users_for_search_widget',\n args: {\n courseid: courseid,\n actionbaseurl: actionBaseUrl,\n },\n };\n return ajax.call([request])[0];\n};\n\n/**\n * Given a course ID, we want to fetch the gradable items, so we may fetch reports based on activity items.\n * Note: This will be worked upon in the single view issue.\n *\n * @method gradeitemFetch\n * @param {int} courseid ID of the course to fetch the users of.\n * @return {object} jQuery promise\n */\nexport const gradeitemFetch = (courseid) => {\n const request = {\n methodname: 'gradereport_singleview_get_grade_items_for_search_widget',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","actionBaseUrl","request","methodname","args","actionbaseurl","ajax","call"],"mappings":";;;;;;;sLAiCyB,CAACA,SAAUC,uBAC1BC,QAAU,CACZC,WAAY,mDACZC,KAAM,CACFJ,SAAUA,SACVK,cAAeJ,uBAGhBK,cAAKC,KAAK,CAACL,UAAU,4BAWDF,iBACrBE,QAAU,CACZC,WAAY,2DACZC,KAAM,CACFJ,SAAUA,kBAGXM,cAAKC,KAAK,CAACL,UAAU"} \ No newline at end of file diff --git a/grade/amd/src/searchwidget/basewidget.js b/grade/amd/src/searchwidget/basewidget.js new file mode 100644 index 00000000000..f4db5e3699d --- /dev/null +++ b/grade/amd/src/searchwidget/basewidget.js @@ -0,0 +1,164 @@ +// 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 . + +/** + * A small modal to search users or grade items within the gradebook. + * + * @module core_grades/searchwidget/basewidget + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import * as ModalFactory from 'core/modal_factory'; +import * as ModalEvents from 'core/modal_events'; +import {debounce} from 'core/utils'; +import * as Templates from 'core/templates'; + +/** + * Build the base searching widget. + * + * @method init + * @param {Promise} bodyPromise The promise from the callee of the contents to place in the modal body. + * @param {Array} data An array of all the data generated by the callee. + * @param {Function} searchFunc Partially applied function we need to manage search the passed dataset. + * @param {string} modalTitle The name of the search widget. + * @param {string|null} unsearchableContent The content rendered in a non-searchable area. + */ +export const init = (bodyPromise, data, searchFunc, modalTitle, unsearchableContent = null) => { + const modal = buildModal(bodyPromise, modalTitle); + registerListenerEvents(modal, data, searchFunc, unsearchableContent); +}; + +/** + * Register chooser related event listeners. + * + * @method registerListenerEvents + * @param {Promise} modal Our modal that we are working with. + * @param {Array} data An array of all the data generated by the callee. + * @param {Function} searchFunc Partially applied function we need to manage search the passed dataset. + * @param {string|null} unsearchableContent The content rendered in a non-searchable area. + */ +const registerListenerEvents = (modal, data, searchFunc, unsearchableContent) => { + modal.then(modal => { + // We want to destroy this when the dialog is closed. + modal.getRoot().on(ModalEvents.hidden, () => { + modal.destroy(); + }); + // Once the body of the modal has been resolved, add more features. + modal.getBodyPromise() + // The return value of getBodyPromise is a jquery object containing the body NodeElement. + .then(body => body[0]) + .then(body => { + const searchInput = body.querySelector('input[data-action="search"]'); + const searchResultsContainer = body.querySelector('[data-region="search-results-container-widget"]'); + + renderSearchResults(searchResultsContainer, data); + + if (unsearchableContent) { + const unsearchableContentContainer = body.querySelector( + '[data-region="unsearchable-content-container-widget"]'); + unsearchableContentContainer.innerHTML += unsearchableContent; + } + + // The search input is triggered. + searchInput.addEventListener('input', debounce(() => { + // Display the search results. + renderSearchResults( + searchResultsContainer, + debounceCallee( + searchInput.value, + data, + searchFunc() + ) + ); + }, 300)); + return body; + }).catch(); + }).catch(); +}; + +/** + * Given an object we want to build a modal ready to show. + * + * @method buildModal + * @param {Promise} bodyPromise Body promise that the caller should resolve. + * @param {string} modalTitle The name of the search widget. + * @return {Object} The modal ready to display immediately and render body in later. + */ +const buildModal = (bodyPromise, modalTitle) => { + return ModalFactory.create({ + type: ModalFactory.types.DEFAULT, + // TODO: Make this defined by the interface. + title: modalTitle, + body: bodyPromise, + small: true, + scrollable: false, + templateContext: { + classes: 'reportdatasearch modal-sm' + } + }).then(modal => { + modal.show(); + return modal; + }); +}; + +/** + * We have a small helper that'll call the curried search function allowing callers to filter + * the data set however we want rather than defining how data must be filtered. + * + * @method debounceCallee + * @param {String} searchValue The input from the user that we'll search against. + * @param {Array} data An array of all the data generated by the callee. + * @param {Function} searchFunction Partially applied function we need to manage search the passed dataset. + * @return {Array} The filtered subset of the provided data that we'll then render into the results. + */ +const debounceCallee = (searchValue, data, searchFunction) => { + if (searchValue.length > 0) { // Search query is present. + return searchFunction(data, searchValue); + } + return data; +}; + +/** + * Given the output of the callers' search function, render out the results into the modal. + * + * @method renderSearchResults + * @param {HTMLElement} searchResultsContainer The DOM node of the widget where we'll render the provided results. + * @param {Array} searchResultsData The filtered subset of the provided data that we'll then render into the results. + */ +const renderSearchResults = async(searchResultsContainer, searchResultsData) => { + const templateData = { + 'searchresults': searchResultsData, + }; + // Build up the html & js ready to place into the help section. + const {html, js} = await Templates.renderForPromise('core_grades/searchwidget/searchresults', templateData); + await Templates.replaceNodeContents(searchResultsContainer, html, js); +}; + +/** + * We want to create the basic promises and hooks that the caller will implement, so we can build modals + * ahead of time and allow the caller to resolve their promises once complete. + * + * @method promisesAndResolvers + * @returns {{bodyPromise: Promise, bodyPromiseResolver}} + */ +export const promisesAndResolvers = () => { + // We want to show the modal instantly but loading whilst waiting for our data. + let bodyPromiseResolver; + const bodyPromise = new Promise(resolve => { + bodyPromiseResolver = resolve; + }); + + return {bodyPromiseResolver, bodyPromise}; +}; diff --git a/grade/amd/src/searchwidget/repository.js b/grade/amd/src/searchwidget/repository.js new file mode 100644 index 00000000000..74689a82ae2 --- /dev/null +++ b/grade/amd/src/searchwidget/repository.js @@ -0,0 +1,61 @@ +// 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 . + +/** + * A repo for the search widget. + * + * @module core_grades/searchwidget/repository + * @copyright 2022 Mathew May + * @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 enrolled learners, so we may fetch their reports. + * + * @method userFetch + * @param {int} courseid ID of the course to fetch the users of. + * @param {string} actionBaseUrl The base URL for the user option. + * @return {object} jQuery promise + */ +export const userFetch = (courseid, actionBaseUrl) => { + const request = { + methodname: 'core_grades_get_enrolled_users_for_search_widget', + args: { + courseid: courseid, + actionbaseurl: actionBaseUrl, + }, + }; + return ajax.call([request])[0]; +}; + +/** + * Given a course ID, we want to fetch the gradable items, so we may fetch reports based on activity items. + * Note: This will be worked upon in the single view issue. + * + * @method gradeitemFetch + * @param {int} courseid ID of the course to fetch the users of. + * @return {object} jQuery promise + */ +export const gradeitemFetch = (courseid) => { + const request = { + methodname: 'gradereport_singleview_get_grade_items_for_search_widget', + args: { + courseid: courseid, + }, + }; + return ajax.call([request])[0]; +}; diff --git a/grade/classes/external/get_enrolled_users_for_search_widget.php b/grade/classes/external/get_enrolled_users_for_search_widget.php new file mode 100644 index 00000000000..37bfc2b0a6f --- /dev/null +++ b/grade/classes/external/get_enrolled_users_for_search_widget.php @@ -0,0 +1,157 @@ +. + +namespace core_grades\external; + +use external_api; +use external_function_parameters; +use external_value; +use external_single_structure; +use external_multiple_structure; +use moodle_url; +use core_user; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/grade/lib.php'); + +/** + * Get the enrolled users within and map some fields to the returned array of user objects. + * + * @package core_grades + * @copyright 2022 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.1 + */ +class get_enrolled_users_for_search_widget extends external_api { + + /** + * Returns description of method parameters. + * + * @return external_function_parameters + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters ( + [ + 'courseid' => new external_value(PARAM_INT, 'Course Id', VALUE_REQUIRED), + 'actionbaseurl' => new external_value(PARAM_URL, 'The base URL for the user option', VALUE_REQUIRED), + 'groupid' => new external_value(PARAM_INT, 'Group Id', VALUE_DEFAULT, 0) + ] + ); + } + + /** + * Given a course ID find the enrolled users within and map some fields to the returned array of user objects. + * + * @param int $courseid + * @param string $actionbaseurl The base URL for the user option. + * @param int|null $groupid + * @return array Users and warnings to pass back to the calling widget. + * @throws coding_exception + * @throws invalid_parameter_exception + * @throws moodle_exception + * @throws restricted_context_exception + */ + public static function execute(int $courseid, string $actionbaseurl, ?int $groupid = 0): array { + global $DB, $PAGE; + + $params = self::validate_parameters( + self::execute_parameters(), + [ + 'courseid' => $courseid, + 'actionbaseurl' => $actionbaseurl, + 'groupid' => $groupid + ] + ); + + $warnings = []; + $coursecontext = \context_course::instance($params['courseid']); + parent::validate_context($coursecontext); + + require_capability('moodle/course:viewparticipants', $coursecontext); + + $course = $DB->get_record('course', ['id' => $params['courseid']]); + // Create a graded_users_iterator because it will properly check the groups etc. + $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); + $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); + $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $coursecontext); + + $gui = new \graded_users_iterator($course, null, $params['groupid']); + $gui->require_active_enrolment($showonlyactiveenrol); + $gui->init(); + + $users = []; + + while ($userdata = $gui->next_user()) { + $guiuser = $userdata->user; + $user = new \stdClass(); + $user->fullname = fullname($guiuser); + $user->id = $guiuser->id; + $user->url = (new moodle_url($actionbaseurl, ['id' => $courseid, 'userid' => $guiuser->id]))->out(false); + $userpicture = new \user_picture($guiuser); + $userpicture->size = 1; + $user->profileimage = $userpicture->get_url($PAGE)->out(false); + $user->email = $guiuser->email; + + $users[] = $user; + } + $gui->close(); + + return [ + 'users' => $users, + 'warnings' => $warnings, + ]; + } + + /** + * Returns description of method result value. + * + * @return external_single_structure + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'users' => new external_multiple_structure(self::user_description()), + 'warnings' => new \external_warnings(), + ]); + } + + /** + * Create user return value description. + * + * @return \external_description + */ + public static function user_description(): \external_description { + $userfields = [ + 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'), + 'profileimage' => new external_value( + PARAM_URL, + 'The location of the users larger image', + VALUE_OPTIONAL + ), + 'url' => new external_value( + PARAM_URL, + 'The link to the user report', + VALUE_OPTIONAL + ), + 'fullname' => new external_value(PARAM_TEXT, 'The full name of the user', VALUE_OPTIONAL), + 'email' => new external_value( + core_user::get_property_type('email'), + 'An email address - allow email as root@localhost', + VALUE_OPTIONAL), + ]; + return new external_single_structure($userfields); + } +} diff --git a/grade/lib.php b/grade/lib.php index 94f4a12ad1c..ce8fdfe1af6 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -2959,7 +2959,7 @@ abstract class grade_helper { } $pluginstr = get_string('pluginname', 'gradereport_'.$plugin); - $url = new moodle_url('/grade/report/'.$plugin.'/index.php', array('id' => $courseid)); + $url = new moodle_url('/grade/report/'.$plugin.'/index.php', array('id'=>$courseid)); $gradereports[$plugin] = new grade_plugin_info($plugin, $url, $pluginstr); // Add link to preferences tab if such a page exists diff --git a/grade/report/user/amd/build/user.min.js b/grade/report/user/amd/build/user.min.js new file mode 100644 index 00000000000..b25c8dbdba2 --- /dev/null +++ b/grade/report/user/amd/build/user.min.js @@ -0,0 +1,10 @@ +define("gradereport_user/user",["exports","core/pending","core/templates","core/custom_interaction_events","core_grades/searchwidget/repository","core_grades/searchwidget/basewidget","core/str","core/url"],(function(_exports,_pending,Templates,_custom_interaction_events,Repository,WidgetBase,_str,_url){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 _interopRequireWildcard(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]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * A small modal to search users within the gradebook. + * + * @module gradereport_user/user + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_pending=_interopRequireDefault(_pending),Templates=_interopRequireWildcard(Templates),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),Repository=_interopRequireWildcard(Repository),WidgetBase=_interopRequireWildcard(WidgetBase),_url=_interopRequireDefault(_url);_exports.init=()=>{const pendingPromise=new _pending.default;registerListenerEvents(),pendingPromise.resolve()};const registerListenerEvents=()=>{const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events);let{bodyPromiseResolver:bodyPromiseResolver,bodyPromise:bodyPromise}=WidgetBase.promisesAndResolvers();events.forEach((event=>{document.addEventListener(event,(async e=>{const trigger=e.target.closest(".userwidget");if(trigger){const courseID=trigger.dataset.courseid;e.preventDefault();const actionBaseUrl=_url.default.relativeUrl("/grade/report/user/index.php",{},!1),data=await Repository.userFetch(courseID,actionBaseUrl).catch((async e=>{const errorTemplateData={errormessage:e.message};bodyPromiseResolver(await Templates.render("core_grades/searchwidget/error",errorTemplateData))}));if(data===[])return;const allUsersOptionName=await(0,_str.get_string)("allusersnum","gradereport_user",data.users.length),allUsersOption=await Templates.render("core_grades/searchwidget/searchitem",{id:0,name:allUsersOptionName,url:_url.default.relativeUrl("/grade/report/user/index.php",{id:courseID,userid:0},!1)});WidgetBase.init(bodyPromise,data.users,searchUsers(),(0,_str.get_string)("selectauser","gradereport_user"),allUsersOption)}}))})),bodyPromiseResolver(Templates.render("core_grades/searchwidget/user/usersearch_body",{displayunsearchablecontent:!0}))},searchUsers=()=>()=>(users,searchTerm)=>{if(""===searchTerm)return users;searchTerm=searchTerm.toLowerCase();const searchResults=[];return users.forEach((user=>{user.fullname.toLowerCase().includes(searchTerm)&&searchResults.push(user)})),searchResults}})); + +//# sourceMappingURL=user.min.js.map \ No newline at end of file diff --git a/grade/report/user/amd/build/user.min.js.map b/grade/report/user/amd/build/user.min.js.map new file mode 100644 index 00000000000..a1f25c6b252 --- /dev/null +++ b/grade/report/user/amd/build/user.min.js.map @@ -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 .\n\n/**\n * A small modal to search users within the gradebook.\n *\n * @module gradereport_user/user\n * @copyright 2022 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Pending from 'core/pending';\nimport * as Templates from 'core/templates';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport * as Repository from 'core_grades/searchwidget/repository';\nimport * as WidgetBase from 'core_grades/searchwidget/basewidget';\nimport {get_string as getString} from 'core/str';\nimport Url from 'core/url';\n\n/**\n * Our entry point into starting to build the search widget.\n * It'll eventually, based upon the listeners, open the search widget and allow filtering.\n *\n * @method init\n */\nexport const init = () => {\n const pendingPromise = new Pending();\n registerListenerEvents();\n pendingPromise.resolve();\n};\n\n/**\n * Register user search widget related event listeners.\n *\n * @method registerListenerEvents\n */\nconst registerListenerEvents = () => {\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n CustomEvents.define(document, events);\n\n let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers();\n\n // Register events.\n events.forEach((event) => {\n document.addEventListener(event, async(e) => {\n const trigger = e.target.closest('.userwidget');\n if (trigger) {\n const courseID = trigger.dataset.courseid;\n e.preventDefault();\n\n const actionBaseUrl = Url.relativeUrl('/grade/report/user/index.php', {}, false);\n // If an error occurs while fetching the data, display the error within the modal.\n const data = await Repository.userFetch(courseID, actionBaseUrl).catch(async(e) => {\n const errorTemplateData = {\n 'errormessage': e.message\n };\n bodyPromiseResolver(\n await Templates.render('core_grades/searchwidget/error', errorTemplateData)\n );\n });\n\n // Early return if there is no module data.\n if (data === []) {\n return;\n }\n\n // The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget.\n const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length);\n const allUsersOption = await Templates.render('core_grades/searchwidget/searchitem', {\n id: 0,\n name: allUsersOptionName,\n url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false),\n });\n\n WidgetBase.init(\n bodyPromise,\n data.users,\n searchUsers(),\n getString('selectauser', 'gradereport_user'),\n allUsersOption\n );\n }\n });\n });\n // Resolvers for passed functions in the modal creation.\n bodyPromiseResolver(Templates.render(\n 'core_grades/searchwidget/user/usersearch_body', {displayunsearchablecontent: true}\n ));\n};\n\n/**\n * Define how we want to search and filter users when the user decides to input a search value.\n *\n * @method registerListenerEvents\n * @returns {function(): function(*, *): (*)}\n */\nconst searchUsers = () => {\n return () => {\n return (users, searchTerm) => {\n if (searchTerm === '') {\n return users;\n }\n searchTerm = searchTerm.toLowerCase();\n const searchResults = [];\n users.forEach((user) => {\n const userName = user.fullname.toLowerCase();\n if (userName.includes(searchTerm)) {\n searchResults.push(user);\n }\n });\n return searchResults;\n };\n };\n};\n"],"names":["pendingPromise","Pending","registerListenerEvents","resolve","events","CustomEvents","activate","keyboardActivate","define","document","bodyPromiseResolver","bodyPromise","WidgetBase","promisesAndResolvers","forEach","event","addEventListener","async","trigger","e","target","closest","courseID","dataset","courseid","preventDefault","actionBaseUrl","Url","relativeUrl","data","Repository","userFetch","catch","errorTemplateData","message","Templates","render","allUsersOptionName","users","length","allUsersOption","id","name","url","userid","init","searchUsers","displayunsearchablecontent","searchTerm","toLowerCase","searchResults","user","fullname","includes","push"],"mappings":";;;;;;;qYAqCoB,WACVA,eAAiB,IAAIC,iBAC3BC,yBACAF,eAAeG,iBAQbD,uBAAyB,WACrBE,OAAS,CACX,QACAC,mCAAaD,OAAOE,SACpBD,mCAAaD,OAAOG,qDAEXC,OAAOC,SAAUL,YAE1BM,oBAACA,oBAADC,YAAsBA,aAAeC,WAAWC,uBAGpDT,OAAOU,SAASC,QACZN,SAASO,iBAAiBD,OAAOE,MAAAA,UACvBC,QAAUC,EAAEC,OAAOC,QAAQ,kBAC7BH,QAAS,OACHI,SAAWJ,QAAQK,QAAQC,SACjCL,EAAEM,uBAEIC,cAAgBC,aAAIC,YAAY,+BAAgC,IAAI,GAEpEC,WAAaC,WAAWC,UAAUT,SAAUI,eAAeM,OAAMf,MAAAA,UAC7DgB,kBAAoB,cACNd,EAAEe,SAEtBxB,0BACUyB,UAAUC,OAAO,iCAAkCH,0BAK7DJ,OAAS,gBAKPQ,yBAA2B,mBAAU,cAAe,mBAAoBR,KAAKS,MAAMC,QACnFC,qBAAuBL,UAAUC,OAAO,sCAAuC,CACjFK,GAAI,EACJC,KAAML,mBACNM,IAAKhB,aAAIC,YAAY,+BAAgC,CAACa,GAAInB,SAAUsB,OAAQ,IAAI,KAGpFhC,WAAWiC,KACPlC,YACAkB,KAAKS,MACLQ,eACA,mBAAU,cAAe,oBACzBN,uBAMhB9B,oBAAoByB,UAAUC,OAC1B,gDAAiD,CAACW,4BAA4B,MAUhFD,YAAc,IACT,IACI,CAACR,MAAOU,iBACQ,KAAfA,kBACOV,MAEXU,WAAaA,WAAWC,oBAClBC,cAAgB,UACtBZ,MAAMxB,SAASqC,OACMA,KAAKC,SAASH,cAClBI,SAASL,aAClBE,cAAcI,KAAKH,SAGpBD"} \ No newline at end of file diff --git a/grade/report/user/amd/src/user.js b/grade/report/user/amd/src/user.js new file mode 100644 index 00000000000..ede80f0574d --- /dev/null +++ b/grade/report/user/amd/src/user.js @@ -0,0 +1,130 @@ +// 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 . + +/** + * A small modal to search users within the gradebook. + * + * @module gradereport_user/user + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Pending from 'core/pending'; +import * as Templates from 'core/templates'; +import CustomEvents from "core/custom_interaction_events"; +import * as Repository from 'core_grades/searchwidget/repository'; +import * as WidgetBase from 'core_grades/searchwidget/basewidget'; +import {get_string as getString} from 'core/str'; +import Url from 'core/url'; + +/** + * Our entry point into starting to build the search widget. + * It'll eventually, based upon the listeners, open the search widget and allow filtering. + * + * @method init + */ +export const init = () => { + const pendingPromise = new Pending(); + registerListenerEvents(); + pendingPromise.resolve(); +}; + +/** + * Register user search widget related event listeners. + * + * @method registerListenerEvents + */ +const registerListenerEvents = () => { + const events = [ + 'click', + CustomEvents.events.activate, + CustomEvents.events.keyboardActivate + ]; + CustomEvents.define(document, events); + + let {bodyPromiseResolver, bodyPromise} = WidgetBase.promisesAndResolvers(); + + // Register events. + events.forEach((event) => { + document.addEventListener(event, async(e) => { + const trigger = e.target.closest('.userwidget'); + if (trigger) { + const courseID = trigger.dataset.courseid; + e.preventDefault(); + + const actionBaseUrl = Url.relativeUrl('/grade/report/user/index.php', {}, false); + // If an error occurs while fetching the data, display the error within the modal. + const data = await Repository.userFetch(courseID, actionBaseUrl).catch(async(e) => { + const errorTemplateData = { + 'errormessage': e.message + }; + bodyPromiseResolver( + await Templates.render('core_grades/searchwidget/error', errorTemplateData) + ); + }); + + // Early return if there is no module data. + if (data === []) { + return; + } + + // The HTML for the 'All users' option which will be rendered in the non-searchable content are of the widget. + const allUsersOptionName = await getString('allusersnum', 'gradereport_user', data.users.length); + const allUsersOption = await Templates.render('core_grades/searchwidget/searchitem', { + id: 0, + name: allUsersOptionName, + url: Url.relativeUrl('/grade/report/user/index.php', {id: courseID, userid: 0}, false), + }); + + WidgetBase.init( + bodyPromise, + data.users, + searchUsers(), + getString('selectauser', 'gradereport_user'), + allUsersOption + ); + } + }); + }); + // Resolvers for passed functions in the modal creation. + bodyPromiseResolver(Templates.render( + 'core_grades/searchwidget/user/usersearch_body', {displayunsearchablecontent: true} + )); +}; + +/** + * Define how we want to search and filter users when the user decides to input a search value. + * + * @method registerListenerEvents + * @returns {function(): function(*, *): (*)} + */ +const searchUsers = () => { + return () => { + return (users, searchTerm) => { + if (searchTerm === '') { + return users; + } + searchTerm = searchTerm.toLowerCase(); + const searchResults = []; + users.forEach((user) => { + const userName = user.fullname.toLowerCase(); + if (userName.includes(searchTerm)) { + searchResults.push(user); + } + }); + return searchResults; + }; + }; +}; diff --git a/grade/report/user/classes/external/user.php b/grade/report/user/classes/external/user.php index a533c783c23..0fb8d6dafc3 100644 --- a/grade/report/user/classes/external/user.php +++ b/grade/report/user/classes/external/user.php @@ -35,6 +35,7 @@ use gradereport_user\report\user as user_report; defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir.'/externallib.php'); +require_once($CFG->dirroot.'/grade/lib.php'); /** * External grade report API implementation diff --git a/grade/report/user/classes/report/user.php b/grade/report/user/classes/report/user.php index 30360b84398..4cfda092297 100644 --- a/grade/report/user/classes/report/user.php +++ b/grade/report/user/classes/report/user.php @@ -1220,6 +1220,19 @@ class user extends grade_report { } } + /** + * Build the html for the zero state of the user report. + * @return string HTML to display + */ + public function output_report_zerostate(): string { + global $OUTPUT, $COURSE; + $context = [ + 'courseid' => $COURSE->id, + 'imglink' => new \moodle_url('/pix/f/clip-353 1.png'), + ]; + return $OUTPUT->render_from_template('gradereport_user/zero_state', $context); + } + /** * Trigger the grade_report_viewed event * diff --git a/grade/report/user/db/services.php b/grade/report/user/db/services.php index f0b88f1f23e..1be3aa7e314 100644 --- a/grade/report/user/db/services.php +++ b/grade/report/user/db/services.php @@ -48,5 +48,5 @@ $functions = [ 'type' => 'read', 'capabilities' => 'gradereport/user:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], - ] + ], ]; diff --git a/grade/report/user/index.php b/grade/report/user/index.php index 38ac35c6e00..b19d04e0d90 100644 --- a/grade/report/user/index.php +++ b/grade/report/user/index.php @@ -27,11 +27,14 @@ require_once $CFG->libdir.'/gradelib.php'; require_once $CFG->dirroot.'/grade/lib.php'; require_once $CFG->dirroot.'/grade/report/user/lib.php'; +use gradereport_user\report\user as reportbase; + $courseid = required_param('id', PARAM_INT); -$userid = optional_param('userid', $USER->id, PARAM_INT); +$userid = optional_param('userid', null, PARAM_INT); $userview = optional_param('userview', 0, PARAM_INT); $PAGE->set_url(new moodle_url('/grade/report/user/index.php', ['id' => $courseid])); +$PAGE->requires->js_call_amd('gradereport_user/user', 'init'); if ($userview == 0) { $userview = get_user_preferences('gradereport_user_view_user', GRADE_REPORT_USER_VIEW_USER); @@ -49,9 +52,9 @@ $PAGE->set_pagelayout('report'); $context = context_course::instance($course->id); require_capability('gradereport/user:view', $context); -if (empty($userid)) { +if ($userid === 0 || is_null($userid)) { require_capability('moodle/grade:viewall', $context); -} else { +} else if ($userid) { if (!$DB->get_record('user', ['id' => $userid, 'deleted' => 0]) || isguestuser($userid)) { throw new \moodle_exception('invaliduser'); } @@ -61,7 +64,7 @@ $access = false; if (has_capability('moodle/grade:viewall', $context)) { // User can view all course grades. $access = true; -} else if ($userid == $USER->id && has_capability('moodle/grade:view', $context) && $course->showgrades) { +} else if (($userid == $USER->id || is_null($userid)) && has_capability('moodle/grade:view', $context) && $course->showgrades) { // User can view own grades. $access = true; } else if (has_capability('moodle/grade:viewall', context_user::instance($userid)) && $course->showgrades) { @@ -89,7 +92,7 @@ grade_regrade_final_grades_if_required($course); // Teachers will see all student reports. if (has_capability('moodle/grade:viewall', $context)) { // Verify if we are using groups or not. - $groupmode = groups_get_course_groupmode($course); + $groupmode = groups_get_course_groupmode($course); $currentgroup = $gpr->groupid; // To make some other functions work better later. @@ -119,13 +122,25 @@ if (has_capability('moodle/grade:viewall', $context)) { $viewasuser = false; } - if (empty($userid)) { + if (is_null($userid)) { + $report = new reportbase($courseid, $gpr, $context, $USER->id); + + if (isset($report)) { + // Trigger report viewed event. + $report->viewed(); + } + + // Print header. + print_grade_page_head($course->id, 'report', 'user', ' ', false); + + echo $report->output_report_zerostate(); + } else if (empty($userid)) { $gui = new graded_users_iterator($course, null, $currentgroup); $gui->require_active_enrolment($showonlyactiveenrol); $gui->init(); // Add tabs. print_grade_page_head($courseid, 'report', 'user'); - groups_print_course_menu($course, $gpr->get_return_url('index.php?id='.$courseid, ['userid' => 0])); + groups_print_course_menu($course, $gpr->get_return_url('index.php?id=' . $courseid, ['userid' => 0])); if ($user_selector) { echo $renderer->graded_users_selector('user', $course, $userid, $currentgroup, true); @@ -147,7 +162,7 @@ if (has_capability('moodle/grade:viewall', $context)) { echo $OUTPUT->heading($studentnamelink); if ($report->fill_table()) { - echo '
'.$report->print_table(true); + echo '
' . $report->print_table(true); } echo "

"; } @@ -163,10 +178,11 @@ if (has_capability('moodle/grade:viewall', $context)) { ), fullname($report->user) ); - print_grade_page_head($courseid, 'report', 'user', get_string('pluginname', 'gradereport_user') . ' - ' . $studentnamelink, - false, false, true, null, null, $report->user); + print_grade_page_head($courseid, 'report', 'user', + get_string('pluginname', 'gradereport_user') . ' - ' . $studentnamelink, + false, false, true, null, null, $report->user); - groups_print_course_menu($course, $gpr->get_return_url('index.php?id='.$courseid, ['userid' => 0])); + groups_print_course_menu($course, $gpr->get_return_url('index.php?id=' . $courseid, ['userid' => 0])); if ($user_selector) { $showallusersoptions = true; @@ -179,20 +195,21 @@ if (has_capability('moodle/grade:viewall', $context)) { echo $OUTPUT->notification(get_string('groupusernotmember', 'error')); } else { if ($report->fill_table()) { - echo '
'.$report->print_table(true); + echo '
' . $report->print_table(true); } } } } else { // Students will see just their own report. // Create a report instance. - $report = new gradereport_user\report\user($courseid, $gpr, $context, $userid); + $report = new gradereport_user\report\user($courseid, $gpr, $context, $userid ?? $USER->id); // Print the page. - print_grade_page_head($courseid, 'report', 'user', get_string('pluginname', 'gradereport_user'). ' - '.fullname($report->user)); + print_grade_page_head($courseid, 'report', 'user', + get_string('pluginname', 'gradereport_user') . ' - ' . fullname($report->user)); if ($report->fill_table()) { - echo '
'.$report->print_table(true); + echo '
' . $report->print_table(true); } } diff --git a/grade/report/user/lang/en/gradereport_user.php b/grade/report/user/lang/en/gradereport_user.php index 943b8df2eb7..19119d2df7a 100644 --- a/grade/report/user/lang/en/gradereport_user.php +++ b/grade/report/user/lang/en/gradereport_user.php @@ -24,11 +24,16 @@ defined('MOODLE_INTERNAL') || die(); +$string['allusersnum'] = 'All users ({$a})'; $string['eventgradereportviewed'] = 'Grade user report viewed'; $string['pluginname'] = 'User report'; $string['user:view'] = 'View your own grade report'; $string['myself'] = 'Myself'; $string['otheruser'] = 'User'; $string['privacy:metadata:preference:gradereport_user_view_user'] = 'Whether to view report as current user or another user in the gradebook reports'; +$string['selectauser'] = 'Select a user'; +$string['selectuser'] = 'Select a user to view their grades'; +$string['selectuserinstructions'] = 'By selecting a user you can view the grades by activity'; +$string['selectuserlink'] = 'Click to select user'; $string['tablesummary'] = 'The table is arranged as a list of graded items including categories of graded items. When items are in a category they will be indicated as such.'; $string['viewas'] = 'View report as'; diff --git a/grade/report/user/renderer.php b/grade/report/user/renderer.php index 48f7c9709cc..640197040ff 100644 --- a/grade/report/user/renderer.php +++ b/grade/report/user/renderer.php @@ -39,12 +39,12 @@ class gradereport_user_renderer extends plugin_renderer_base { * @param string $report * @param stdClass $course * @param int $userid - * @param int $groupid + * @param null|int $groupid * @param bool $includeall * @return string The raw HTML to render. * @throws coding_exception */ - public function graded_users_selector(string $report, stdClass $course, int $userid, int $groupid, bool $includeall): string { + public function graded_users_selector(string $report, stdClass $course, int $userid, ?int $groupid, bool $includeall): string { $select = grade_get_graded_users_select($report, $course, $userid, $groupid, $includeall); $output = html_writer::tag('div', $this->output->render($select), ['id' => 'graded_users_selector']); diff --git a/grade/report/user/templates/zero_state.mustache b/grade/report/user/templates/zero_state.mustache new file mode 100644 index 00000000000..f7d6b046dc1 --- /dev/null +++ b/grade/report/user/templates/zero_state.mustache @@ -0,0 +1,37 @@ +{{! + 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 . +}} +{{! + @template gradereport_user/zero_state + + The zero state of the user report that contains the image and trigger for the search widget. + + Example context (json): + { + "imglink": "http://foo.bar/gradereport/?userid=25", + "courseid": "2" + } +}} +
+ Temp +

{{#str}}selectuser, gradereport_user{{/str}}

+

{{#str}}selectuserinstructions, gradereport_user{{/str}}

+ {{#str}}selectuserlink, gradereport_user{{/str}} +
diff --git a/grade/report/user/tests/behat/user_view.feature b/grade/report/user/tests/behat/user_view.feature index a6c174bad72..ded04bff316 100644 --- a/grade/report/user/tests/behat/user_view.feature +++ b/grade/report/user/tests/behat/user_view.feature @@ -49,7 +49,7 @@ Feature: View the user report as the student will see it Scenario: View the report as the teacher themselves When I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And I select "Myself" from the "View report as" singleselect Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | @@ -65,7 +65,7 @@ Feature: View the user report as the student will see it Scenario: View the report as the student from both the teachers and students perspective When I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And I select "User" from the "View report as" singleselect Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | @@ -107,7 +107,7 @@ Feature: View the user report as the student will see it And I set the field with xpath "//select[@name='report_user_showtotalsifcontainhidden']" to "Show totals excluding hidden items" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - When I select "Student 1" from the "Select all or one user" singleselect + When I click on "Student 1" in the "user" search widget And I select "User" from the "View report as" singleselect Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | @@ -145,7 +145,7 @@ Feature: View the user report as the student will see it And I set the field with xpath "//select[@name='report_user_showtotalsifcontainhidden']" to "Show totals including hidden items" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - When I select "Student 1" from the "Select all or one user" singleselect + When I click on "Student 1" in the "user" search widget And I select "User" from the "View report as" singleselect Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | @@ -191,7 +191,7 @@ Feature: View the user report as the student will see it And I set the field with xpath "//select[@name='report_user_showtotalsifcontainhidden']" to "Show totals excluding hidden items" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - When I select "Student 1" from the "Select all or one user" singleselect + When I click on "Student 1" in the "user" search widget And I select "User" from the "View report as" singleselect Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | diff --git a/grade/report/user/tests/behat/usersearch.feature b/grade/report/user/tests/behat/usersearch.feature new file mode 100644 index 00000000000..543e34460e9 --- /dev/null +++ b/grade/report/user/tests/behat/usersearch.feature @@ -0,0 +1,33 @@ +@core @core_grades @gradereport_user @javascript +Feature: Within the User report, a teacher can search for users. + Background: + Given the following "courses" exist: + | fullname | shortname | category | groupmode | + | Course 1 | C1 | 0 | 1 | + And the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | t1 | + | student1 | Student | 1 | student1@example.com | s1 | + | student2 | Student | 2 | student2@example.com | s2 | + | student32 | Student | 32 | student32@example.com | s32 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student32 | C1 | student | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I change window size to "large" + + Scenario: A teacher can search for and find a user to view + When I navigate to "View > User report" in the course gradebook + And I click on "Click to select user" "link" + And I confirm "Student 1" in "Select a user" search within the gradebook widget exists + And I confirm "Student 2" in "Select a user" search within the gradebook widget exists + And I confirm "Student 32" in "Select a user" search within the gradebook widget exists + And I set the field "searchinput" to "2" + And I wait "1" seconds + Then I confirm "Student 2" in "Select a user" search within the gradebook widget exists + And I confirm "Student 32" in "Select a user" search within the gradebook widget exists + And I confirm "Student 1" in "Select a user" search within the gradebook widget does not exist diff --git a/grade/report/user/tests/behat/view_usereport.feature b/grade/report/user/tests/behat/view_usereport.feature index 6a3495c1b9f..75ee486a29b 100644 --- a/grade/report/user/tests/behat/view_usereport.feature +++ b/grade/report/user/tests/behat/view_usereport.feature @@ -13,5 +13,5 @@ Feature: We can use the user report Given I log in as "admin" And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook - And I select "All users (0)" from the "Select all or one user" singleselect + And I click on "All users (0)" in the "user" search widget Then I should see "There are no students enrolled in this course." diff --git a/grade/report/user/version.php b/grade/report/user/version.php index 11a52d9f6d5..01abb90baf8 100644 --- a/grade/report/user/version.php +++ b/grade/report/user/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2022041901; // The current plugin version (Date: YYYYMMDDXX). +$plugin->version = 2022041902; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2022041200; // Requires this Moodle version. $plugin->component = 'gradereport_user'; // Full name of the plugin (used for diagnostics) diff --git a/grade/templates/searchwidget/error.mustache b/grade/templates/searchwidget/error.mustache new file mode 100644 index 00000000000..c4dc44fb8a1 --- /dev/null +++ b/grade/templates/searchwidget/error.mustache @@ -0,0 +1,39 @@ +{{! + 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 . +}} +{{! + @template core_grades/searchwidget/error + + Chooser error template. + + Variables required for this template: + * errormessage - The error message + + Example context (json): + { + "errormessage": "Error" + } +}} +
+ +
diff --git a/grade/templates/searchwidget/searchitem.mustache b/grade/templates/searchwidget/searchitem.mustache new file mode 100644 index 00000000000..de0e4db5450 --- /dev/null +++ b/grade/templates/searchwidget/searchitem.mustache @@ -0,0 +1,48 @@ +{{! + 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 . +}} +{{! + @template core_grades/searchwidget/searchitem + + Search result line items. + + Example context (json): + { + "id": "1", + "name": "Quiz 1", + "url": "http://foo.bar/gradereport/?userid=25", + "fullname": "Cameron Greeve" + } +}} +
+ {{#name}} + + + {{name}} + + + {{/name}} + {{^name}} + + + {{fullname}} + + + {{email}} + + + {{/name}} +
diff --git a/grade/templates/searchwidget/searchresults.mustache b/grade/templates/searchwidget/searchresults.mustache new file mode 100644 index 00000000000..40d40a5c527 --- /dev/null +++ b/grade/templates/searchwidget/searchresults.mustache @@ -0,0 +1,46 @@ +{{! + 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 . +}} +{{! + @template core_grades/searchwidget/searchresults + + The wrapper in which our search results will be rendered + + Example context (json): + { + "searchresults": [ + { + "id": "1", + "name": "Quiz 1", + "url": "http://foo.bar/gradereport/?userid=25", + "fullname": "Cameron Greeve", + "sendmessage": "http://foo.bar/message/index.php?id=25", + "addcontact": "http://foo.bar/message/index.php?user1=2&user2=14&addcontact=14&sesskey=XXXXX", + "currentuser": "2" + } + ] + } +}} +
+
+ {{#searchresults}} + {{>core_grades/searchwidget/searchitem}} + {{/searchresults}} + {{^searchresults}} +

{{#str}} resultsfound, core, 0 {{/str}}

+ {{/searchresults}} +
+
diff --git a/grade/templates/searchwidget/user/usersearch_body.mustache b/grade/templates/searchwidget/user/usersearch_body.mustache new file mode 100644 index 00000000000..9f5be04520a --- /dev/null +++ b/grade/templates/searchwidget/user/usersearch_body.mustache @@ -0,0 +1,36 @@ +{{! + 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 . +}} +{{! + @template core_grades/searchwidget/user/usersearch_body + + The body of the user widget. + + Example context (json): + { + "displayunsearchablecontent": true + } +}} +{{< core/search_input_auto }} + {{$label}}{{#str}} + searchusers, core_grades + {{/str}}{{/label}} + {{$placeholder}}{{#str}} + searchusers, core_grades + {{/str}}{{/placeholder}} +{{/ core/search_input_auto }} + +
+{{#displayunsearchablecontent}} +
+{{/displayunsearchablecontent}} diff --git a/grade/tests/behat/behat_grade.php b/grade/tests/behat/behat_grade.php index 6ba91e5946d..a3c0971e1a6 100644 --- a/grade/tests/behat/behat_grade.php +++ b/grade/tests/behat/behat_grade.php @@ -421,4 +421,59 @@ class behat_grade extends behat_base { "#{$formid}", 'css_element']); } } + + /** + * Confirm if a value is within the search widget within the gradebook. + * + * Examples: + * - I confirm "User1" in "User" search within the gradebook widget exists + * + * @Given /^I confirm "(?P(?:[^"]|\\")*)" in "(?P(?:[^"]|\\")*)" search within the gradebook widget exists$/ + * @param string $needle The value to search for. + * @param string $haystack The selector to use within the zero state. + */ + public function i_confirm_in_search_within_the_gradebook_widget_exists($needle, $haystack) { + $this->execute("behat_general::wait_until_exists", [$haystack, "dialogue"]); + $this->execute("behat_general::assert_element_contains_text", [$needle, $haystack, "dialogue"]); + } + + /** + * Confirm if a value is not within the search widget within the gradebook. + * + * Examples: + * - I confirm "User1" in "User" search within the gradebook widget does not exist + * + * @Given /^I confirm "(?P(?:[^"]|\\")*)" in "(?P(?:[^"]|\\")*)" search within the gradebook widget does not exist$/ + * @param string $needle The value to search for. + * @param string $haystack The selector to use within the zero state. + */ + public function i_confirm_in_search_within_the_gradebook_widget_does_not_exist($needle, $haystack) { + $this->execute("behat_general::wait_until_exists", [$haystack, "dialogue"]); + $this->execute("behat_general::assert_element_not_contains_text", [$needle, $haystack, "dialogue"]); + } + + /** + * Clicks on an option from the specified search widget in the current gradebook page. + * + * Examples: + * - I click on "Student1" in the "user" search widget + * - I click on "Group1" in the "group" search widget + * + * @Given /^I click on "(?P(?:[^"]|\\")*)" in the "(?P(?:[^"]|\\")*)" search widget$/ + * @param string $needle The value to search for. + * @param string $haystack The type of the search widget. + */ + public function i_click_on_in_search_widget(string $needle, string $haystack) { + $this->execute("behat_general::wait_until_the_page_is_ready"); + if ($haystack === 'user') { + $this->execute("behat_general::i_click_on", ['.userwidget', "css_element"]); + $dialoguetitle = 'Select a user'; + } else { + $this->execute("behat_general::i_click_on", ['.groupwidget', "css_element"]); + $dialoguetitle = 'Select a grade item'; + } + + $this->execute("behat_general::wait_until_exists", [$dialoguetitle, "dialogue"]); + $this->execute('behat_general::i_click_on_in_the', [$needle, "link", $dialoguetitle, 'dialogue']); + } } diff --git a/grade/tests/behat/grade_aggregation.feature b/grade/tests/behat/grade_aggregation.feature index a7649219291..ca396878bed 100644 --- a/grade/tests/behat/grade_aggregation.feature +++ b/grade/tests/behat/grade_aggregation.feature @@ -53,6 +53,7 @@ Feature: We can use calculated grade totals And I give the grade "10.00" to the user "Student 1" for the grade item "Test assignment eight" And I give the grade "5.00" to the user "Student 1" for the grade item "Test assignment nine" And I press "Save changes" + And I change window size to "large" And I set the following settings for grade item "Test assignment two": | Hidden | 1 | And I set the following settings for grade item "Test assignment five": @@ -349,8 +350,8 @@ Feature: We can use calculated grade totals And I set the field "Show weightings" to "Show" And I press "Save changes" And I navigate to "View > User report" in the course gradebook + And I click on "Student 1" in the "user" search widget And I select "Myself" from the "View report as" singleselect - And I select "Student 1" from the "Select all or one user" singleselect And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Contribution to course total | | Test assignment five | 28.57 % | 10.00 (50.00 %) | 0–20 | 1.03 % | @@ -541,8 +542,8 @@ Feature: We can use calculated grade totals And I navigate to "View > Grader report" in the course gradebook Then I should see "75.00 (16.85 %)" in the ".course" "css_element" And I navigate to "View > User report" in the course gradebook + And I click on "Student 1" in the "user" search widget And I select "Myself" from the "View report as" singleselect - And I select "Student 1" from the "Select all or one user" singleselect And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Test assignment five | 57.14 % | 10.00 (50.00 %) | 2.25 % | diff --git a/grade/tests/behat/grade_calculated_grade_items.feature b/grade/tests/behat/grade_calculated_grade_items.feature index 91f4870d922..da234625a12 100644 --- a/grade/tests/behat/grade_calculated_grade_items.feature +++ b/grade/tests/behat/grade_calculated_grade_items.feature @@ -42,7 +42,7 @@ Feature: Calculated grade items can be used in the gradebook And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | @@ -69,7 +69,7 @@ Feature: Calculated grade items can be used in the gradebook And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | @@ -82,7 +82,7 @@ Feature: Calculated grade items can be used in the gradebook And I give the grade "65.00" to the user "Student 2" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - When I select "Student 1" from the "Select all or one user" singleselect + When I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | @@ -99,7 +99,7 @@ Feature: Calculated grade items can be used in the gradebook | Min and max grades used in calculation | Initial min and max grades | And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | @@ -135,7 +135,7 @@ Feature: Calculated grade items can be used in the gradebook And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - When I select "Student 1" from the "Select all or one user" singleselect + When I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | 66.67 % | 75.00 | 0–100 | 75.00 % | 50.00 % | @@ -149,7 +149,7 @@ Feature: Calculated grade items can be used in the gradebook And I give the grade "65.00" to the user "Student 2" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | 71.43 % | 75.00 | 0–100 | 75.00 % | 53.57 % | diff --git a/grade/tests/behat/grade_calculated_grade_items_20150627.feature b/grade/tests/behat/grade_calculated_grade_items_20150627.feature index 88558150a30..5fcd2e103d5 100644 --- a/grade/tests/behat/grade_calculated_grade_items_20150627.feature +++ b/grade/tests/behat/grade_calculated_grade_items_20150627.feature @@ -43,7 +43,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | @@ -70,7 +70,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | @@ -83,7 +83,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 And I give the grade "65.00" to the user "Student 2" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - When I select "Student 1" from the "Select all or one user" singleselect + When I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | @@ -100,7 +100,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 | Min and max grades used in calculation | Initial min and max grades | And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | @@ -136,7 +136,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 And I give the grade "75.00" to the user "Student 1" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - When I select "Student 1" from the "Select all or one user" singleselect + When I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | 50.00 % | 75.00 | 0–100 | 75.00 % | 37.50 % | @@ -150,7 +150,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 And I give the grade "65.00" to the user "Student 2" for the grade item "grade item 1" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | 50.00 % | 75.00 | 0–100 | 75.00 % | 37.50 % | diff --git a/grade/tests/behat/grade_calculated_weights.feature b/grade/tests/behat/grade_calculated_weights.feature index 72fee663783..18eae783b7c 100644 --- a/grade/tests/behat/grade_calculated_weights.feature +++ b/grade/tests/behat/grade_calculated_weights.feature @@ -62,7 +62,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "Course 1": | Aggregation | Mean of grades | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -87,7 +87,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "EN Sub category": | Item weight | 1.0 | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -108,7 +108,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "EN Test assignment three": | Extra credit | 1 | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -127,7 +127,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "EN Test assignment three": | Extra credit weight | 1.0 | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -144,7 +144,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "Course 1": | Aggregation | Median of grades | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -161,7 +161,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "Course 1": | Aggregation | Lowest grade | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -178,7 +178,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "Course 1": | Aggregation | Highest grade | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -195,7 +195,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "Course 1": | Aggregation | Mode of grades | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -216,7 +216,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "EN Test assignment three": | Extra credit | 1 | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: @@ -235,7 +235,7 @@ Feature: We can understand the gradebook user report And I set the following settings for grade item "EN Test assignment three": | Extra credit | 1 | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget # Check the values in the weights column. Then the following should exist in the "user-grade" table: diff --git a/grade/tests/behat/grade_contribution_with_extra_credit.feature b/grade/tests/behat/grade_contribution_with_extra_credit.feature index cb53856f38c..6998bbe42ff 100644 --- a/grade/tests/behat/grade_contribution_with_extra_credit.feature +++ b/grade/tests/behat/grade_contribution_with_extra_credit.feature @@ -64,7 +64,7 @@ Feature: Extra credit contributions are normalised when going out of bounds And I set the following settings for grade item "Manual item 4": | Extra credit | 1 | And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | | 80.00 | | diff --git a/grade/tests/behat/grade_grade_minmax_change.feature b/grade/tests/behat/grade_grade_minmax_change.feature index 2cc6119ca90..e6b3fd58e50 100644 --- a/grade/tests/behat/grade_grade_minmax_change.feature +++ b/grade/tests/behat/grade_grade_minmax_change.feature @@ -48,7 +48,7 @@ Feature: We can change the maximum and minimum number of points for manual items | Maximum grade | 10 | And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | 100.00 % | 10.00 | 100.00 % | @@ -64,7 +64,7 @@ Feature: We can change the maximum and minimum number of points for manual items | Maximum grade | 20 | And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | 100.00 % | 20.00 | 100.00 % | diff --git a/grade/tests/behat/grade_hidden_items.feature b/grade/tests/behat/grade_hidden_items.feature index 911dd5e2a9a..186cbb6c0e2 100644 --- a/grade/tests/behat/grade_hidden_items.feature +++ b/grade/tests/behat/grade_hidden_items.feature @@ -55,8 +55,8 @@ Feature: Student and teacher's view of aggregated grade items is consistent when And I press "Save changes" And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook + And I click on "Student 1" in the "user" search widget And I select "Myself" from the "View report as" singleselect - And I select "Student 1" from the "Select all or one user" singleselect Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 100.00 % | 50.00 | 0–100 | 50.00 % | 25.00 % | diff --git a/grade/tests/behat/grade_hidden_items_locked_category.feature b/grade/tests/behat/grade_hidden_items_locked_category.feature index aaec7a37472..9a4b061c2bd 100644 --- a/grade/tests/behat/grade_hidden_items_locked_category.feature +++ b/grade/tests/behat/grade_hidden_items_locked_category.feature @@ -47,8 +47,8 @@ Feature: Hidden grade items should be hidden when grade category is locked, but Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook + And I click on "Student 1" in the "user" search widget And I select "Myself" from the "View report as" singleselect - When I select "Student 1" from the "Select all or one user" singleselect Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test locked category total | 100.00 % | 50.00 | 0–100 | 50.00 % | - | diff --git a/grade/tests/behat/grade_mingrade.feature b/grade/tests/behat/grade_mingrade.feature index b385b3d27ae..5f3fd1d8330 100644 --- a/grade/tests/behat/grade_mingrade.feature +++ b/grade/tests/behat/grade_mingrade.feature @@ -100,7 +100,7 @@ Feature: We can use a minimum grade different than zero And I give the grade "0.00" to the user "Student 2" for the grade item "Manual item 6" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | 18.18 % | -25.00 | -4.55 % | diff --git a/grade/tests/behat/grade_minmax.feature b/grade/tests/behat/grade_minmax.feature index 60cc12aa54d..36a520936d9 100644 --- a/grade/tests/behat/grade_minmax.feature +++ b/grade/tests/behat/grade_minmax.feature @@ -76,7 +76,7 @@ Feature: We can choose what min or max grade to use when aggregating grades. And I give the grade "10.00" to the user "Student 2" for the grade item "MI 3" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 1 | 20.00 % | 75.00 | 0–100 | 75.00 % | 15.00 % | @@ -106,7 +106,7 @@ Feature: We can choose what min or max grade to use when aggregating grades. | Maximum grade | 50.00 | | Minimum grade | 5.00 | And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 1 | 12.50 % | 75.00 | 5–50 | 100.00 % | 18.75 % | @@ -131,7 +131,7 @@ Feature: We can choose what min or max grade to use when aggregating grades. | Rescale existing grades | No | | Maximum grade | 200.00 | And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 5 | 40.00 % | 150.00 | 0–200 | 75.00 % | 30.00 % | @@ -145,7 +145,7 @@ Feature: We can choose what min or max grade to use when aggregating grades. When I set the field "Min and max grades used in calculation" to "Initial min and max grades" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 1 | 16.67 % | 75.00 | 0–100 | 75.00 % | 12.50 % | diff --git a/grade/tests/behat/grade_natural_exclude_empty.feature b/grade/tests/behat/grade_natural_exclude_empty.feature index b23be1f4533..0304ca87eac 100644 --- a/grade/tests/behat/grade_natural_exclude_empty.feature +++ b/grade/tests/behat/grade_natural_exclude_empty.feature @@ -42,7 +42,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 28.57 % | 80.00 | 0–100 | 80.00 % | 22.86 % | @@ -63,7 +63,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 66.67 % | 80.00 | 0–100 | 80.00 % | 53.33 % | @@ -82,7 +82,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 0.00 %( Empty ) | - | 0–100 | - | 0.00 % | @@ -109,7 +109,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 0.00 %( Extra credit ) | 80.00 | 0–100 | 80.00 % | 0.00 % | @@ -134,7 +134,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 50.00 % | 80.00 | 0–100 | 80.00 % | 40.00 % | @@ -158,7 +158,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 83.33 % | 80.00 | 0–100 | 80.00 % | 66.67 % | @@ -180,7 +180,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 0.00 %( Empty ) | - | 0–100 | - | 0.00 % | @@ -207,7 +207,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 50.00 % | 80.00 | 0–100 | 80.00 % | 40.00 % | @@ -233,7 +233,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 83.33 % | 80.00 | 0–100 | 80.00 % | 66.67 % | @@ -257,7 +257,7 @@ Feature: Weights in natural aggregation are adjusted if the items are excluded f And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 0.00 %( Empty ) | - | 0–100 | - | 0.00 % | diff --git a/grade/tests/behat/grade_natural_exclude_empty_20150619.feature b/grade/tests/behat/grade_natural_exclude_empty_20150619.feature index 39578ff9a38..039e3951695 100644 --- a/grade/tests/behat/grade_natural_exclude_empty_20150619.feature +++ b/grade/tests/behat/grade_natural_exclude_empty_20150619.feature @@ -43,7 +43,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 28.57 % | 80.00 | 0–100 | 80.00 % | 22.86 % | @@ -64,7 +64,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 66.67 % | 80.00 | 0–100 | 80.00 % | 53.33 % | @@ -83,7 +83,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 0.00 %( Empty ) | - | 0–100 | - | 0.00 % | @@ -110,7 +110,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 0.00 %( Extra credit ) | 80.00 | 0–100 | 80.00 % | 0.00 % | @@ -135,7 +135,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 50.00 % | 80.00 | 0–100 | 80.00 % | 40.00 % | @@ -160,7 +160,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 83.33 % | 80.00 | 0–100 | 80.00 % | 66.67 % | @@ -183,7 +183,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 0.00 %( Empty ) | - | 0–100 | - | 0.00 % | @@ -210,7 +210,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 50.00 % | 80.00 | 0–100 | 80.00 % | 40.00 % | @@ -237,7 +237,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 83.33 % | 80.00 | 0–100 | 80.00 % | 66.67 % | @@ -262,7 +262,7 @@ Feature: Gradebook calculations for extra credit items before the fix 20150619 And I give the grade "8.00" to the user "Student 1" for the grade item "Test assignment five (extra)" And I press "Save changes" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 0.00 %( Empty ) | - | 0–100 | - | 0.00 % | diff --git a/grade/tests/behat/grade_scales.feature b/grade/tests/behat/grade_scales.feature index 72a55042146..834370ee921 100644 --- a/grade/tests/behat/grade_scales.feature +++ b/grade/tests/behat/grade_scales.feature @@ -86,7 +86,7 @@ Feature: View gradebook when scales are used | Range | F–A | 0.00–5.00 | 0.00–5.00 | | Overall average | C | 3.00 | 3.00 | And I navigate to "View > User report" in the course gradebook - And I select "Student 3" from the "Select all or one user" singleselect + And I click on "Student 3" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | Contribution to course total | | Test assignment one | C | F–A | 50.00 % | 60.00 % | @@ -132,7 +132,7 @@ Feature: View gradebook when scales are used | Range | F–A | 1.00–5.00 | 0.00–100.00 | | Overall average | C | 3.00 | | And I navigate to "View > User report" in the course gradebook - And I select "Student 3" from the "Select all or one user" singleselect + And I click on "Student 3" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | Contribution to course total | | Test assignment one | C | F–A | 50.00 % | | diff --git a/grade/tests/behat/grade_scales_aggregation.feature b/grade/tests/behat/grade_scales_aggregation.feature index 7632b5c3797..05c27b186e8 100644 --- a/grade/tests/behat/grade_scales_aggregation.feature +++ b/grade/tests/behat/grade_scales_aggregation.feature @@ -47,7 +47,7 @@ Feature: Control the aggregation of the scales And I set the following settings for grade item "Course 1": | Aggregation | | And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | Contribution to course total | | Grade me | 10.00 | 10.00 % | | @@ -61,7 +61,7 @@ Feature: Control the aggregation of the scales And I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | Contribution to course total | | Grade me | 10.00 | 10.00 % | | diff --git a/grade/tests/behat/grade_single_item_scales.feature b/grade/tests/behat/grade_single_item_scales.feature index 3852ad57083..9cd97bf9c52 100644 --- a/grade/tests/behat/grade_single_item_scales.feature +++ b/grade/tests/behat/grade_single_item_scales.feature @@ -66,7 +66,7 @@ Feature: View gradebook when single item scales are used | Range | Ace!–Ace! | 0.00–1.00 | 0.00–1.00 | | Overall average | Ace! | 1.00 | 1.00 | And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Grade | Range | Contribution to course total | | Test assignment one | Ace! | Ace!–Ace! | 100.00 % | @@ -104,7 +104,7 @@ Feature: View gradebook when single item scales are used | Range | Ace!–Ace! | 0.00–100.0 | 0.00–100.00 | | Overall average | Ace! | | | And I navigate to "View > User report" in the course gradebook - And I select "Student 1" from the "Select all or one user" singleselect + And I click on "Student 1" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Grade | Range | Contribution to course total | | Test assignment one | Ace! | Ace!–Ace! | | diff --git a/grade/tests/behat/grade_view.feature b/grade/tests/behat/grade_view.feature index 6a1ac1a4f77..ec74521b2e8 100644 --- a/grade/tests/behat/grade_view.feature +++ b/grade/tests/behat/grade_view.feature @@ -71,7 +71,7 @@ Feature: We can enter in grades and view reports from the gradebook Scenario: Grade a grade item and ensure the results display correctly in the gradebook When I navigate to "View > User report" in the course gradebook And the "Gradebook navigation menu" select menu should contain "Grader report" - And the "Select all or one user" select box should contain "All users (1)" + And I click on "All users (1)" in the "user" search widget And I log out And I log in as "student1" And I follow "Grades" in the user menu diff --git a/lang/en/grades.php b/lang/en/grades.php index fbb78ed66b7..84319617303 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -725,6 +725,7 @@ $string['savechanges'] = 'Save changes'; $string['savepreferences'] = 'Save preferences'; $string['scaleconfirmdelete'] = 'Are you sure you wish to delete the scale "{$a}"?'; $string['scaledpct'] = 'Scaled %'; +$string['searchusers'] = 'Search users'; $string['seeallcoursegrades'] = 'See all course grades'; $string['select'] = 'Select {$a}'; $string['selectalloroneuser'] = 'Select all or one user'; diff --git a/lib/db/services.php b/lib/db/services.php index c3ebbffc178..6299fb02747 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -956,6 +956,13 @@ $functions = array( 'type' => 'write', 'capabilities' => 'moodle/grade:manage', ), + 'core_grades_get_enrolled_users_for_search_widget' => array ( + 'classname' => 'core_grades\external\get_enrolled_users_for_search_widget', + 'description' => 'Returns the enrolled users within and map some fields to the returned array of user objects.', + 'type' => 'read', + 'ajax' => true, + 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], + ), 'core_grading_get_definitions' => array( 'classname' => 'core_grading_external', 'methodname' => 'get_definitions', diff --git a/mod/h5pactivity/tests/behat/grading_attempts.feature b/mod/h5pactivity/tests/behat/grading_attempts.feature index 14f9e7a8f7d..ee7b38db7c6 100644 --- a/mod/h5pactivity/tests/behat/grading_attempts.feature +++ b/mod/h5pactivity/tests/behat/grading_attempts.feature @@ -45,7 +45,7 @@ Feature: Change grading options in an H5P activity Scenario: Default grading is max attempt grade Given I am on the "Course 1" course page logged in as teacher1 When I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 100.00 | 100.00 % | @@ -57,7 +57,7 @@ Feature: Change grading options in an H5P activity | Grading method | First attempt | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 0.00 | 0.00 % | @@ -69,7 +69,7 @@ Feature: Change grading options in an H5P activity | Grading method | Last attempt | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 0.00 | 0.00 % | @@ -81,7 +81,7 @@ Feature: Change grading options in an H5P activity | Grading method | Average grade | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 33.33 | 33.33 % | @@ -93,7 +93,7 @@ Feature: Change grading options in an H5P activity | Grading method | Don't calculate a grade | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | - | - | @@ -105,7 +105,7 @@ Feature: Change grading options in an H5P activity | Enable attempt tracking | No | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | - | - | @@ -118,7 +118,7 @@ Feature: Change grading options in an H5P activity | Grading method | Average grade | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | | Awesome H5P package | 33.33 | 0–100 | 33.33 % | @@ -130,7 +130,7 @@ Feature: Change grading options in an H5P activity | Maximum grade | 50 | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | | Awesome H5P package | 16.67 | 0–50 | 33.33 % | @@ -143,7 +143,7 @@ Feature: Change grading options in an H5P activity | Grading method | Average grade | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | | Awesome H5P package | 33.33 | 0–100 | 33.33 % | @@ -155,7 +155,7 @@ Feature: Change grading options in an H5P activity | Maximum grade | 50 | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook - And I set the field "Select all or one user" to "Student 1" + And I click on "Student 1" in the "user" search widget Then the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | | Awesome H5P package | 33.33 | 0–50 | 66.67 % | diff --git a/pix/f/clip-353 1.png b/pix/f/clip-353 1.png new file mode 100644 index 0000000000000000000000000000000000000000..3bcaff57f0be3acd422fd0abd6016e9712909c5d GIT binary patch literal 29979 zcmb@sWmFu|5-y6n1}8Yd-66o>3&&2=u5z;{!C<;+IL3I3a0AnH|CjtRc9fSO$4+{a|SS2kk^4%5kOdCEE zr~mbR{Jg`tUFF=j0Fl-of!-rFief6m8R_~Unef8?p5d(zT3gu8gv)C-B3&t&k`;hH(1>R*6 z8Q^1qrR%Lvq#rAxAZYFE_5?*hcI$|Fe*5Umj z{bW2p1Yjex4U74Z1rFv_)blQjru}0gGM@C2!;ZrZ)AX_!M>&u2rupk;rp0IW(i}{l z>c48$25V2Zr_Zmi|LzY!S&>6&zfABPPjHXKyy^VuJXu<7<+E7$5Qp)ku3SqUGQ)9^ z=@l`*WnX*k~U^<+@3 z6bSsMl-`5#gvmtK3|_Rxds3r@gE*YTUd6$o_>Kfesi5LY-QD4@St5+o+a4@;r&SXx z_KA8cccTTX?j4Cvv_#`NO@}Xtdsy*s7`uZp=({)M&9>VHU6)0f3R7QjH4l&!QR4TY zms26n_gj9}R>I8f)6wB%d8m?tTL9Ehiq z5jb6_;P?HXfHZ^^?^AzGfMkx<{YX72Y9X_la#nHG#Gd5&ziap>$JA2bO!HfJM*`54M zYy{Qnq$eT@nO(VNf^LZ9)5N#&HC4Pji(2ah_2EhB@oOwO#pF7{`#&(-&mFNE>(xBK zPRV~pJ)LHvnmd8qdwb0TTy$7y;0V11m zNzL}1t<0|iBi>Rjl&f>Bamsy0P)K7h3j3!v?UbMeUg?1U=*_D1 zk*csGp+bY;HnREYXMaNxZWQj$!hWPy(-FV%!lTox!+JfCN+!85_TOae zRwn0uJJ4-KbA0)W3-dw}z;r~8ol8~Rhc+*E!SWgP1Sq>G##AvU+Gy@BH5 z;&1=4Pwryqq`yp*Drfwn^yte5NOR1@c+=QhMyCD%fo{Xw2!lR-u~J*J$$fXgD}Hun zcJ|)maX-atovC-8wApsr;pX?m;kb;~A6%W2W`X#k945IDT*6n@%zVLb0T>Db;J)NA z!vRa2U6UNIJ9T!K^EGSCksp7!qc^Mc#lHRv*^B;On;->Byw=E&4W3RJW@mzTR|a>K z<|V5p$h}UO)=%!x2C1t#%vz3E=X#HyK($z->g+#1oc1$r?Q~L7QYFD=P=s|C6`(!P zB-`_<;`M&h$53BipXOun9hvPVEB+UpaHrQ_j`)A5s1z9Gw}{?@&v+Ao(plqgH7HlP2kl$Dj` zG+Uxz@`*Rt8GS5{9o&jAs5$5Gy`Z9gl^Spr?N*a`!-SH5Tquo}fA^OohLt&A5auCU zfLAVmeNc?oRkx9*CBbbQ$wJEwYaH`SiNEbd%JJfYJZYJijkw>@?5(fqg<7Vv*}W}! zw3ywCuRB`U#j(Q))rYKUA+yVM9(sZ@B+=|0kFEuB`U$11nu^}VD~Oh+Dl~2m%4LlG zjkCsHc*GBwx9qO^CmfRkvz}-5i|lrws7rE2gNDB7>>abZ+nK#U;70V@Pj=+W2$v#U zZS&xl;>9X7jI8b$Z%>?eGw*L*FFygA;}j}T+I?v`b5OM*}dSbpz{15(C5 zvrztDHK~}GlCasSjGkcki|y3V+3aB~mDrMbqf4nv0cE8+@GP?X9H9#Q@+7vnkSJG- zC*+(Z6<~DZ6kp)#w5nBumCZb|O9!J@L*!437H-6xY1CgH!5*l!pGapt*tv=>{m}Ou z4kxj>>P&o}d5OI;B?V!86s8P*jac8<{hq|EU9g0d(J&Cx=`Rf7F)5rX^zkMee77~u zu@@3K2?F=VP82p+#LnfJ_2oCqw}-y%c)DU3FE$Q8y6(E3Kzat}a*|FTxJ&0GFP|9d z86+go)8d>A8jtB(iAdjCkxjC?E08itoz6hc?-hlK87?~ETb|7UL+Qjq>K$jQl{5ju zf)MXY_Orr*xw8i6)~b0HSJ@!!Gioik=>>Cn4@-U1%!*TmZSM-Ev&Nts&nfHKCr-f@ z7SlOQ(MDr)<=Jw_*S6Y{Fk`|!VWAjn^O6L{c%LE?3I+*9d?;5|dY3B_)3e3py?fqG zP3r97z~h2?m9@s|1p(MNU2+YS3&`K zXRlg4DIC(n;)siW>pz#MwzCmU9I~U|=$=KK@XK9@mzN;BnRLHQ;{tjqx z%#-FTcbbhVLD%O7RrHxTPv3EaSIVKS;`K6ad%IQb0*9%m4ksl!Qa?Dg>nB)MZ6<`5%ep}uBsYV`AoffbA%C_cRT19Aea2bnZtPCioNh;Wu4#P_WO`ovLk8(PghI z8|?M>DHHJV>`c9kGrmvvp%0hvH7sMt&9PVA)+$WK`A)Q4P+sT}ddqrY$ilJIsK5e8 zkqV6NuN3hVxGrBG$k3T0aOO=YR8huoS)k+U$FdcpVPI?qO5p~XRfQ4I!Y9wxuev_l zOTpK#5nz+s-EoX>CiYjF!jSJ+tb?NwU z7R6?3wn)D#US~YBLw24WLs4_oi;Z3tbr?w<1xG~VZh)&6 zeU*BB-cM!dAZbyoX4AKjqd=}?IwA1Mbdl0Fx@m(GhV0q%mWKeghCXRIk|rq zRz){^9zDm;dDk(7+y<$op zxMN(S7mASH6u%APv^ql{wVClonH?uK?rPP^OnzdxSDPLT|0Pm%I{}9w2ib$Tw+wpyQnL*#c%N;>ZpCRQKy<Qp1aM9h@2)DWM$Bphz=5HmtIfe5LYgAp@(P~<$o&9tb zopzBkHC)c3Eu-4<6xi|c5}_Z{N+FnO3^HgmY(IgX^ci71?6nOEsOx@naM|jJ9(8p_ zZN}3@Z6O$YNp2jC1Uw3HE6eBRhyoC~HQCHtsx{Ld_}onO+~u`w|2ThY+3k&G|83gI zWGE?ma*h6Kfb4;*Mh7I+w<9P8e&sezon9z)g3Gd+IiTYUd3d zQh@LYI{X<$&U8@UduZd5FpI+`J0TT z_8hf3+Z;hp7^KO>J-K=#_VfV`OJfNQq|qM&QxdM zQ~U0H-9C|arZY&^n&R{h9Vd zhdo?Y!kyv30*whk-M{KW&k@)9`v+CUXpgKLyngNw3v<_3;Uvu=y$?xA6!PKgl^_`< z7%QeOGg*r%z+C*!&lB<*{WJ56vU(Y;7^8<{YpsRc3aT3%=%-Y#GPD!c;~^>WJKjq4 z2(aZbQby~WoCv6ePB8ZsFNNu7cYZEJ*aJQAK`@{4*E@V-E^w3qD!8`IzoM@dNjrR= zpv+MowSI9I@V;#P3Ronr~vff1fsR*oy@fPYH$~iWYSNgpYt`*5~z7;Y@uYH#N$(%y` zcg)!^rp%4&B)9YG)?3t#?={%;rfrge_dwalHW__|+Y38#B!b-hZm8uD_OQ3|Gl;+8 zk1v95`VP)J8SuWApIoOw)CZIF=g*)pJjOfmu=c4P>ZegtdN#-dF-V)W_3)&-C}msL5cF1Wgr0$O?)ZWmsw~sVY+e z_~GFyqjxb<4t_B^_RE1vtvCVA!FY7E#iXcKab0XRRC$FOQHi%}!wshEyS!zq-3KPV zkI2rSQ_)14=LA0xA@pHSgw%(CcI7m+@e}&3|ROcvzYBzhGoFnmDk?Jmb#~`#N(GIWeZ2GT{S*j!M8vr;9zkL%03tZY* z6VcIo7WSeLo*cq|^dO?^Y-ome}- zeKPNf*!@CH@??EiO63-p@h1?jO4oqqA?FKE?mkFwAJ z;p}1LYs0x{7z-7H?wa8l?g^>2L{ono|u z_3E!KS{9JQ(uldI+Qn!i66IP7=;uD#y`COKR~E|4&#N6Wd|0kopuFk^*js|lLETqh zs`xKZM(5nM5BJX1PW}Nx{{#4z3PLA+T@g?r_P9;(yaL+Ge5g=6?$GAwm$l(C#4=Xy zvnPXG_N(A;Nj`Z1maN3isdBL4H;^UaR1NR(cY3St^2FUPPyYYF zAv4%+yQkmxqfY(@Gh#IL4SwKx&`>E4AMmDu@c)HR{!gg?p^pErCr7knwAr+7&Q5CL z+5`xi-y?lMo;PU9!mi4m{+6UlwDfiuX|HV=A^*V7rP17gQ$$zdY+!`;>X+eY-cEHg zQD-4g?e0HJ^^U#JcPZ3rj|tKsA&FvWD(e-z;;(Eqx13_@^r0z8LQkFs>7=K%exN&$ zcAf=$ah4!1{F3z^>ry$VXaC>w84&3OVa(MGbZLZNzYwJN61W*AifpCaA>1PVLu>nW zps2s{nTckpmtX~}?aN>gHBG9pSI}(H^CncKa8#d zsr)th@7;V6qb7H41s{hwxY#2nrCPh_?rDsg#VRPBJq}Q^~ez$ z6Y3eu*AzGMWB0XJ_df!lqRXA^Aav$sLUgvnfAsOg8+VZPvd|rbHvnG%%!mKr@-xwa z1)p0StsM@jsT7XL4;UJ=a*3-;?~wVfS~0vsewhVx@5852Y{L|UeA^{vJ)^IpwO0eC znUQ*HxZrJ+=|G43AA9eCs8#(ZraLgCSLMndaA2?Osrf#!Q4H8uh5Tb81b^wgZ9w(` z7Xjb~Wu&=P?eU>;7izOK0m^br<5@Rv@#9}KPNWU) zgjZZofK@~jeIeWTyrfdr>G(@o7cTx*y9KsKP;vzT^rgN0=hB}0J_A+f-?@5A+p@K@ zLlUHUnx0S&f>1t)GBSD8aX=DECftB&J9$46gytJ>1fjX00W=c_j{}mir3{Bzw$#sN ziusgZl3w})*_pkMD+_vady=V5*mof`qt0He_}1zoIz$=)KE1o@U2EfEs24NPIok4N z>0cDw(cCzYhf)L?2OeDk`QG9Zkwe(Ocn=h_i{LmS#zrn1FzEUnw7SQfoDrwCAX)`4 zyR9+6!{ijs`utOC48J$TLL;X_83Gw{uD1@%Wv_!)wsMn@|>Bl zUTZMN8~7vgl&Eqb{7MJ?O~Xd5tDhg}yCHtIP-?L!X5(LFAu?uT)KTZL zyvR&JVK&pjJMo!L@vpK6p5IP>iQ<8cmKqgH=dYNZQEyqbE`PF@3+)v&_exI^>>0eM zLh|o<7AxeO6YyA1py4r;Yqg(?%JD3u&oJi*xEXX}WFN=_0f$SxV~=x<{mVuxlzPsF zV0CsdqGaZs^Pw%5grJ!En33U^n-Hd~31c@9|3XIc?Ct3ydXIzs0kTts#0E*ezu!3!H9uP8+S{CTi#OhrUyw?PP^sdbvy-E4Z1%*`+(MgY8ak-y{?! zC2e_>V(|gI+dGH8rFZcm)Xzaol8PR%rh!3v(zgJ4E^tsD>jw*$&eML?HpS^VK*+O| z{`(2h0VvCRGl%!?mB%V@*BJ}qDR_!|z7VpbyroPPG&bP)GP`_}eM{LcD055IA)8}p zRe)zeO8pm--DZ@g?MlYHu_ ztdp5(tO+BRl-gEca_NC{(rTB%`3)U`1@N22$ZU#*yK(VPg!e5sDXp5)tBU>-o(g&# zlE1?qj9QbLC9GeL#urluLBs~f?TtL~qkGWzxYUgo9rpQq<52WcQp{Xoi@9PMrMwC$ z;1A?1-NkPS9b>88Sf5)6=1gm`Gg*X0sWi) z1q%9ZL?VAeYPfVH(-z-Mlt!(_6qKs(IBXO%{ff@a>$!B}R*5TE&di+sXdr2@FOIJeU<-6y;hOp>L} z<~>i(=b6E&_k8^izh_ld=CvG-13zWGv#+pn#l=yD(6Z#^vd_1$UNfIq;Cb~aUOLGC zYWCRGRD5yHS&?K;>Yi+IGf-}`^iRU;K~s(P&h-Fne*at@#|%(`fz%$e3BdWIn$vR_9*)E?eyAze>KDZDE4{d=4Na6ikN=VFd(x<|T}Q4N>ljKv zQUsH(C+@^SVul09%^*t6Uz1JMwZEfs1@N4mufp09w+tunpgI4m`p%*4Wc_t4D+}JD z+u22lCSw)#!m^z`Ig9i6|ENXj+2MX${z@aKF}MDL5!LrHx$k2Kjym5vBvwjom5wHu zD0bCpcSo$y;}9!G)c^KTa}`D zZl|Q^UPtk?=}(3~3WL{RRX`L8WIVV5DR&;#wip^{Bj<^48GXnZ4W*$hMm0N#;4HFC zD*4xEuh3k`%reD>d~{nhJj9-(k8f_!c3oe)>Bywq_s!&^b0Ei==lx}W!H5irXNFlX zN8cbMj>KFuR8}#6TiyI2B#>lfumQ>ecGx3sZ5td)mt5$-me_G?WPi)^Q5108t=e=Z zwZ?D8uY4v0wC>UVF$lhZfK9`u5%|7JAHX2vMH6tTHF=NGTp6>1otWH~@r3^`YwRb1 zxI1IxN;gR05|{c4BX(cHKRRjAEO~&0!_h{z8$qE)WK}IOJVqtKWKNn+7d+Rv+$h$Q zk(5$-Cgz3kFDsq~`sS-?HrLeUR#w(|-+`9fTUgE*M#ocxm7R}%nVOaH(H-(t6mdLE1?Fk?Dw8}Re$%@jI4pSMn~@{iWq zzdo7OA-g)IxqXrvLDWr-xhIWa$Qu3JfHqpw9#krMg~ou{I_XldY?W_!6E#hVSCk&Ks{guh|aWN$^NtDCVaF-=*#<+C{g*${#a_ z`}_y~uFv2}9+jNG+iOfa4rAl?iIowgK)GF0GXfJ*hgvt14|X+Q6Q9jM-1hyo^}_Pu z>guSd=MNj+JX6oq(l%&B39>R|_u&EMup{2^xla_`;e&IIt~5#gQV&b+XqG>jqJES5 zKxAJr7TeP@Ustf$>C$X!A-`Bjq0J(2kbETYkDUH%u>C2mB+fZ#DTr+}e_ry)P3bQ($DtfF z-iwBoNrIZ-Gw$`9;YED&-bYynhsM$NL1lthpHYtsl9@D#_#Cb8NF7WaxbSohLHIL#6y$P6yRew7j*2YuorssT8OeQojMND$uZBvgUS;e?PQ#>W~bK^7IGVEb7!=0ZpA%%KNU|0=ZCNmYQ zlp`u9@aCGPpnCc#Bglb$(-qo1GrkKOP==>&MBKqdm1;dSYlnC7s8v0 zL(vQe#=au$@UV%@=*`nmt+=x$LH{?-kz8)4b6KVC``fd-p0>IUJ8ls7A%|DCY#hnv zZSZ;XbWMeYsPoaJtm{K#Xx-$LbjRld`2~a36QJA3 z4Zh)K(Gd7rWvA1k`?&zo0~Zb)KxR69E-*?m$!H+qyk)~Q?9D0fI@fMxqg5^HUWPtp zH34sYL*qE!(I3C^LwWJ9+5i_#Ol&3@*nmnC9rOx`{Jo)R4%F~0-N7ySBu3&o@OlRPM5yH*WB0f%s%I2=+;fb z`yF?AQ6pOI`bVH{kf ztpuW#Rq{XPXcIA_6}qNu)c})?BOhB2W#@h5UttO+EL6p&6MJobPUB>?w*-r_uIt9% zMa@$kYywRpV82vQJ)<^y+uxCI=NRdhbK?RimMtqJtjFzR5dW@Th}2z6FhSKCz< zo2kThpqO3rRdjT`;YlIqw!L?USGCmCu=iiw>vnZ=adEvmtToxye+&%4@afPE9Ek}D z*Z3or(dj_hpR7QZXe~gL6?S!O^r=%!d{7 zmzLz`$d{fBCna|e=2li&zAo;H7blUCk?sq0b@laUSV%#Z0*!t-n$-qDJRBSjU#?P3 zJ?Ml~m6hXz*k{0=u18aO-xhsMW~W)w6f+yP_;7}!pJC4K^A%QU?Sh#;#fvQI?tvQX=+d8D{4wYCy~kw z?*8sOtILNPK+W{Vcz&4Q_H3KB^d0TK6PSjsPe%71G&lF1H2Y8>roemFz>HGyp5_5U zL@J_)IOr<78?31OC?u&qx&3$=Kk^PO&q97C+o9XyNcX$^9qUmC_()?*8*)4a*g|wUO zyM-UBMe92;m50Vm4|OgFuF70`aF^tvD?s_tOK7I_jY9c#&{I(r!5jwufc04fD*i+q zv1Lkq4Q+Nf3~sG$#O;W?p(m7gn(iU>zl_WyfncFCWpRA6xmVzNSu`3ooR{ACDhRJUrc~ z+Wt3;0?+E@<;Ey`@4FOV`S}CDCQA48a~C~ot1E_w=yp$38{PNpchRc02D)*9${0o) zq3j`BjrWfvM8-6sgY!+YVj6rUc~L*eEM-BkD>z|Ew}=~ds3RA$^cH}Tldk43Wfkw@ zI@F362@xV8>FLr`9hT4Jo@ATFBv4S39wliBrkQM$xZb@TP#p8=gU z8dH&Rv7^eF0v5mf{as}G($fkkl#UK!I<}@J63D~98*mdpZ|v0TzVG+P#K1f|6V{;J z|3dw-pEe$z&6=nF^{h)tVclVHx#A&J2%b%SOsI-C@Y&My#>S9)qbpiY>KV;@hK*OJ zN3X{U3tbQQuWTsGWSgtJaM>E;|Hw| za)}@9d5DP;4jw$E zo7rN9xJ^~)HIGuEfh#hzZ8xJi#3WxKNZH^?^5NgAZJ($*9}g?^jJ^8Drc-_!Ir@fo_8rL{E>Z6SJ`f0NyEEvDG~1|YL89>|e+!WU>wmY~4M zYD#tGZpx05@?pzH&!3Q^?Syh_=E@E}T@=Q*5Se2REh#D~S==#&0rQ7nbT`WhH(T-Y z%2`2$oX*<9fQe4?up1*b-$LtXXLO=df=+RGzs#_Z;{|+L{gkZ=Ftn(3raGf`oOv2c za04#oE!9^py5Yt)IfJ{_eUh>tDkql6Mp_)Hccrzp+QLqBt~ z>jdMhMO!P6^jj=iHt#hVmi+uuxW&_BT5}axBkRg%CcvXP}EEqP3xx z3SKKz`N-O0-OqFm3ORMF(U<8XSh?$ryJS#lL#-t_e`0CS(e{)?tP=KLHAwAffFoO6?! zT3VL1QaGc%L}`Wzk?JFAi3Ls7HJ_2b;RWEmG=k1IFXt!AI}N(t{M{i%75R(*(9y`^ zP|YbURW9AGnA6Gt_3q^IOR8L_52!`3LPUQ_lk1*&)Cvzn*30As^^^0jD2bhy_otI$v&b(BCty+rg30x?bDt*=}+u1Ji7y6CSRz{TgC? zCe=pphdzCqVaq-wn$EG_RsvD|%%LOTu3Q&-0&{W{B7>O{qaF7ol=ev#>qf10w z9FehT6YW%$JMe^uM)GQvwU9zJ;WnQ6qUj6i68JMJL@s};??E=^Gb)i;mTa{<(OOuw zF@`d9isj_GMX`J-dj0ICp?EIPaTtRP(+CS>7!jgq-i&KB|7fBomQv-{-VyLeB$yy4 zD&Rvk%K40y@+p(jyOq=ocLS`ZtWH{k5G1BvDh!&n)efQz_Z%FY8QA_h1dp}7M-S|luZC<#zIYi@dd73oECO32EJ z!;kJ?14|J-^C;Y=v<3lp=ipjN^l1Jmi6gRA9lK}0#g#Av1cs*<_Qpw%H}MHI#mi?V zM37^j*NJRP*^yGSzkDmP?L>3K(p=1c>CBr5l2Y}+_7g)i*n0iw7WL~&>facqSj=hq zwiVt}eQ&R6ZH&ouoOMBQkwt4Gro8fQYs*JEkZtI1%fjqb*Z8%06AbEo5c~{u17%le zl>5vGbM7V!Sntjb&1T-Lm6V5w`goLG8ED7(^~cS$o!bfn=zVO>WKLp9-~o_?o^AuYUMZX}=2q&P z0t>c~3E}Miz~rox_iI0rF^CTyNU{*+;CaJBH$+OnHL%;|O%=5bh;I*=WDF8|?`oP% zMtX;GN-#E9E-Rx{=HbKWA;Sgi5UREHkPY>R=<<0!*cx38K&FC+>cM*SAK{BAZ6|P!*_BhoUcq)H1A~3A= zl*)4crsiTLLx>;-GKndp^#>l>w(lE)`|Z74df@>&d@t(ZK{rAOCVHQ9jRi(E4g-_* zveFn6Mf~`8;ngzV1tkXAzQc9>UoR$NH{2S0IMMheR0!w?&Yu*Nubp5{l$t;4*uAai zp(Wc{@1p7%!b8kbzemlTYsA(~z=ENAnEXrIOG7Y`v%tCAGK6 zgZ$vbE|Cpilb^dU@b+w_s&%-g=>nRzUBdc$m#nevr!>;AcsLn!1c#3*=#;RO5}%OGi+DR-p^P&$%6| z5!-LpP;i{GNfs9Tt=#G}kF^%I_?GVPcYl!|CKxr}Fmx750=ub#3@#2^YKI!Aj@w7U z(T}bdHHG&b5uD7O6lFrqBx!cjj0=mSUFtRUEE~~iiM=u`pYpLs%tQ%Ou)g30e2&*B zje*cVYsb-MwTRHLeFgqc5sb$Q__PDgusI`P$PL`l6K+m1{ zWD>b^uY7bh$*BI?*!T~_5FGfEqahUJbxm?YDJ=IdB$F&{f0v# z6~DU%Cu|cYjA$L#5WH$&3yaK)S_^)()EIpAlj;n@$>Qx8EVeE5`rP@B&0){S8|J_( z)D*01*dRpmf#_m1FXaR|(YCdp;tSl&J>ysoy=Vd+V(+7MAvLJZedU! z`jqJWtZ#uyYo-ZTyg_Qc$qcxy010)4ftjV60c+UnxMaq=Oh1>%-s_7mOnU7U#fy*V zD+6c|z0<+9({Me43FH(F{q<4cpcdLw-nodyKNZ{DeJf6cGvSvYgFVf*kIcew zrOwPGkN6lPASa~594jJ%_F8rillQGV!pbmzWznumv)gOWpiBQ~rMB2;GfZTjX2Ngk zQ}+FfvfF#vY(Lxy<;|0^PmrPH^y8wABrxY|2)ljF2B&guvh0${!>SPPYgNqq)!?jm ztM{2UdWBYVe0^@2r!b1*q=+NZ4ge2G4HxnL>vy`zGA+Wo%@3FSXI~E!)3M zYzI*h!bA^ayl+1bKXfEBMkFI+OTcVuA zd*#G?{z77W50=i;`=)p3PzMcAGac`1+I4?UFBc0n{IB`;Z^0n zM6+TGDUl7{p~%V^TPK5a{9YEyTlh_9T!e_ zGa8x=FVqyeu!@+)90v7AE>=|2wK3twi$SZZA_&F>-~R-AEW5+)WKBBx&1j5&xAf|z z%fTBQnC+^F>Tw{LO5=(o;;W%w2z~{ZL++>y%WyTJk}bg7s#oDwnv8A@M1AJaSNb04 zIuuKOzB41@&!al56j%CMX}iFE_mwreag0xxRNgBPKnbVftscG5uJ3xU8*M0&<8B+Ci7=%?cxxe z+z=ie3pYQI7QI8jToxZvn80CeGockPROdUwV}meE1|GvZ=ZiCEO3dLKCc`_$oZq7T zE4q~{<;F4SA;jcb7$N(xdkhBFtyy073#o8g#_}kk8*`TpD8d0ALCS+Mcxo|jBeI1S zC_Q(9Uq~G2`FWRuW~L+HYfKLqGuwmaZXzeo5*=aO^G*_8e|Yd&9fbqj0q%GS*-j!`qqyWYUz7#KaoX_sJYtj>hSL0v{)ufk0 z#XaXy&fxb(a}_|@6|vjqXkImbC>=gyuU({QWm62&-!YOH$k-+OG$$-x;!sblGGxp2 zgIn#eR4UrZkmp@nHxxJ(&;2_LlmQjRf%2sjEBW0bLLvrQa-Hq$U6?Ff2A($^G?`1v zJ(dFxlm!8;oTvaN`L`tpEZfPJEROgQ0~;e_m4yC3HWZsIb{pT?pgLllU*Y<$V%64D zlWS?NevsiC8r}|xJk^&gK!sq(-1DdPGiCbpKBcP>dgx#GtU2rk64+5>ND@Kdvb@c_7GtooWpX^ulH4ca_;gBZ9|8)M!zk zaTujax-b_0E~6n-2oT*2)25eGmiv<%>HFpH`JPDfWBtYB;av!p3j0p9vx3lT9q(D) zY`GdC7PSsT|AZ@;6StZ7ul-(xAYP7`KO1;F0`gBLRDw7iX%n6hFC4#h@gWuJl$l$X zQJ|~EnyWVN4rN~{L|^8_gL@5=X2agD4s@PbbcEBYx}mXfq+z>q4`RH8Q0g&9d;w++ zJ5gWIVgpwJV7$7esxq9I^Hj+ji=UA~h(&q6+pEF|j7njMhJeSAqAuN;sRd$7?G%Z2 zJ>__wuWqih(PtFbmdhv%H_l7`b_Pe`ro?15fHGYHaoY-2ZQuZu_6jL!qRhWV1e=`% zM#*3$p4ad0CodBCd3n1Db7=l<${-mCdHeZ6&Oo>a@mq24V!T39h{I~e#J=ODR`9Q` zuKEl@<3q=*w&sL)D??B3!db&a5D;ZX5=F#c<%K1=5O^>d`mPVNc`6Pl(kjdoGNbGW zRRcmK2ojy>StLR=?S<8%`}tGF)ciQ3DTWN@UF_)ne;h-HI=4BBi4jKVwCxYSyMpOY zbrh2|xS`s|3Wyd%@OpOcrhX~GEzqHXThMtE>_ZK02Hx?t@N5B$a0|XtjFI(c_=QFe zFO;~NX{IQofETqjkLgMtUNj|8yLlG=23rmy-5|Ar1m|hu6x=>TKMcaH-i|VkZri8< z>&UQVr=_IVvP#x2svP|E9+!i~@(KQ|k(^(p6thO%Q7tdnnXSoohxot?7?s3PjBP%P z64jT1(T(Q!kaSS64pqFWBG23PKzhqw*AqFkigm^R^CaD}1mHUviyh|dMuSwQEo9T6 zF=C62hHICY+O5yNl#wPu6izo~ORConA5z?vtBR!C;7C6C*VfIuZ7fyIfncT+O99$s zTjYtw&W$R>YM&ErDX7lkp(}SJ(#Qzv-h=2m{`a^6Q3M?x^$F@Vhh`9~6qddYTN7X4 ziLO#3rQz*xIm3co-WZL+KE3Z{72j)` z>uOigWyuYGh%Ky%gDZ%*9Qzl=<0tH8x9S$VBUNf~#C22zB$f>?tj_hc8x6F#ckU+! zCA>AE@h6JCA6Bs4het6o%dIb40qD$RcmZgG8~B3k2Ezk93*nQF+WdUjMW{*%&KZ}i zU8qC~UJ%e6`)LaL+;RyCT?3<>Cq`9GKU7zRGkE^yheu@qbf3ZYX&mQ0C}RV&3D<0^ z3fenSP{!_e%nJ##mqZK>%MQFE(_Mm99CJxBDa`fpSM~aEEC&{2S6S6{w~QK9RPaOr zpMXB$Qi9Zmu zQocd6xsv>!0DUup%$(8zc&%_HF%EW6>+@jc>Oqr^C>cEKPTM(8{!xTmQIw9vTs4%9>=jU_8bQj zyBOy*qlNG3u50yVxm^o&6w= zu*GUKbSeeN$Fn?hQxC8h!r~7^820CB#XJ5koYmz=;5Y~bq!dKx+1NXNh&*{1r0+ls z`2=+gAdMB!UX=GFo6xy@a1n|V0l~X}_UzeQ-|=|ou`x%#r($It&ek;4cOCCO5w9=W zHJ+_tnN3CMiu;X0Jx$X)KIAwVO(b<=s<}k1>O)8q5<9^T1 zcw05XiT85@h={V51p)!K4zaj$BYsoj6$aUUG3UAKc*jUi!}Zr#l*0Ry12X-F9qdqs zisb`>;>G=6aQRo1p~N>39yV(-LHygaY17hUWwFlb9xg9l5Z=eSsrymit#*CD`%Xvt z-|>4wbVPeEVNs0rSxH$R#PK+OQx1mUrZRa~4Lti4>ZQah5Ta>#=07+;vSGspHN%1E zd=IDpA^k5&eRLO#Vdx_j2MNR`fcHPgVi*Xo65r7G#k{LVY-f$WTDa(#7{FxW)sPDpE_RAzZjK%WbMkDA zqDc!cF-||?Zkg@vwC=`0PP5zeZ!#y8od30Dq4AdHc@^V)m-hDpZM)!vX~zB3i`eeM zQUjb&e!51LO+9zT$sJli*-#@fL1}A)#ml0R9*yS3`KcW;fKpC@lKOV$K!h8Q*oBLo z1J+?FF|kXFH##6s(EbrlZ(+F8Ijz@|-)R57vppA#zPJ zMRbQ;$}v;LTB+%?VggLJ0M4q{T`YD;5t#c)^Y6a$S(T;n);qiO2o(2XF1c{wVu#Q* z5A=Uxxg=5j^wYwMMramnqo05n4SV~Nl=7WDJ9mGv5z@ymyZI`XRMM%JvD0`y87YFA zI(kR)F|=JPM)R*v%u`^V5}cZQniw(;XdxUwO7m}zUrLqS|QB3H07o673s*HQiy zvgESjVTMvLmNl4X+qTTLNG=+i@9e`~QD^;13eApa^xW7SQC8U~M@CKi6isN>$b4~c zyuRzV$b7Fs%DHf49AW24N}ObU>PLZh>t2$CgD zG7FrEsOzBcjKZ!hsoyV`fpI?4Yx5~Gxob-#Uuh|c;$lkd&%Q7gm0`9J+fn|*mZe$a z@OOU;`OiA`X6u)hX78&LZ(g81N8P~O1N6II{nR`OqT7jImKVzF-SQRM9(F1V<$(AY zI`shzAfb1sIb(4WFjpL-$p*xRFZPrNXfI$ zz&p!2gEFx+#$(Y9x+b^irkx<N=IQtz4-81+MI{Z65qS< zSaBxW`z|J(Bk%j6Jh!~wZY^t2(HLmS66wKnpW|q(te2-cWyzssb$0FeVcMZW(3e^^ zk*C-7uRG;@*ynddjz$~nc7F|Mz)(tOnBBYkMAw!#douh1?dniF{aN@$VtuhEDvmZm z8LER$+~tv()Kc9jlxOsaY*IUVv|7f;=^}c7Zh#uZX3~hIJ>eD6_cZBAxEk12 z!=o>-P3v2_ttYnA@p`ofLSgGoA@oGtZP-mO)D6l4r=!AnjLD|<@<#g{<^Sf@6}J)5 zj5Quf7YJ!ZDl&D&SqNQQfKU79I1=8|rcF;6LafvUCH6%Fb8|Z;{32Vk-ixP2bt?Cm z;!5^K2}6sj%CoJHL2R$nW8Ubh4MKa#vh7y9D-qWlWj_q!{yx%GdE>@Ww5&HGed-H{8 z+t46K=r(-^10e#DMtO4AG|DMqqQj9^S(q)<>|IeWk1>Tc#zXq5!d#&OdX?9iKg1u; z6A_U&$K|G!*Vf9ku&~t3W*YiSOteb21dDZ{vGP7RVu<%85o6)YI^=K>v}U3zp%Dc_ z?blvfAD*OZ-qF!$v~tW4uNoLU1Cws%+m=-_A|~oo)$uDO6b%4YJ6zqWLKl*-}uD zrU+Gd=>kJR^`bZY#gA)O3R#GCf$REn`NxvK(6Qx#H$lQz^ zO0lEVfp;ikR#DMe#4C4vh18NCi#SkPCx;P8UvaS`HZgaQGom9WV5i!{h6%MRE&F>_ zR#KqdN0Ev7&U1LDU(0R@*iD&1)J+kiYAI7ajifg=Qkcc#_&34g8K2uHP-up;e26Mh z=H?2Lul=3E9NFP1DfRDyHm+Pra`I$H1bgZ!qjHyG7kcB8orRG{ zftj<9EhS-o6WRWFsdKXg(1Lrhz}xz1W03l~FWP4ZNf^BKf2wWMh7R3=v9N!lsw2I^&W zJAiy<0Ghdgu2>@|2C2DIQeCSJg|w}l5qE}7E7s7WXa7fSb_#`S)cQzDNlT;i|8(sR zo)E!~XrND`r}Y(VgUi!AQyEdQ$TTPtFUwgD=_r$ z7s5{L4hvn?dzc73TC#YhiPuTmn{8dJNp3H=_y1uMSS595*I1`N#j{W+2Sg;I`dHJv zbyC_3@IslQkrG2KUEi$R_F}xdR_)Nm3%4YdptF_=Tk8@-r6|#b9XVzq{pXw`jQ_H3 z2p#({W~zXt3$=tu4@FGj^;96QhRKJCPgt6kg?2O~YjJd2yB!Mav$#)aJCLHQ4ZVeK ziCtTzi>>oOHm7QguJQ(aQ4#wKVpr+v_4MSO_r=QNm_ZPz^KxnKnLnh0kt4P%cH!dr z%T7-J27LvvsWLP{_Zs_vEVYcx7K)qOTo;+HX`cM3NmXNp2(=DnbFDlA))}uiLK|6x zU~Zwbo({)g1EWE!S%gp@mqC+Q69dZ*Z;>2kD-LutBV{CN7Z-ZfGK`7}StL^^Cu~yZ zxB_SQfzoCKw2HKqDZvPO(>#G4fm`_hBt@j*<9TZ&k8fg)Jzvg}~Ql&C>Kamx2%r^SJFAxT!Hi*hgUN-x|6;l(7m41*}4OJ!tdj0AUwPs&MUfZ zpBGOPntT|f`OSz&g+-;s736_9wumhl;1kr28LcupNk-)dAlGkHwy8k#6!FowFn*VS z<9H%omhHDMSC$=3Cd4+~lndFiV|pC14Wi31Sam*U%T8t!O^D}X^RUv$W}J>tLN&>^C1@4cs{{RY_5~HAuYPOK_)gNU=ugQifUMQ zLgh{JXq9h^+z*zVARp!BQt2@^*P7o&z79N}P>`3^S-;1m*;-a-{=2Kb_A|)SR9b6Y z?bKIlxNm@%QjfYE;`E(7u)IOmKwnj|q1ti2+gxLw!SyJswHGIsk7uhO20ctkKsmP~ zy&R%Y!g`22FO;FY(gxEJ4KdMduD3s85vr_Cs?fKXEXFw^l)YN%yMRqK(c@W!LYave z;hSr%3(;4fl+{|dCDr9tJoiyqjrD)Ab_ulMJ-qYXikiq&=0R=ST1Y!WIJlmA83sOv z=P>*2HkZL%QbC4Bd1)GLsIjP|&IzsKPf4Nu4?9?KHOwV(Sk;|5R$?OlG#p6%%4{kq zJrFA_bFqjT3DzC0pQqX4AybOVL`K^>8$4(pSbZhR99l%7vPS#6cGNPwsa_?k229V2 zsBWDB4#_M`*Oho@3!0Dr&(@{hWqH9nAxkV zBZ7|1=&@}yX=T=0ZOSy@q2|a2%irom6l2`30E5f=XwuU15QlHPh2fwy*?NwwE z+j9u!PO#Z@<-d6V2NwfV`=_Qib6{fVu}E>9%af;PcdHV2-$z4 zZGaa>XOW@bD#< zoRo4a?O`ab3Qy8J-l@W%EhDmpQc&`Yg}xbq&uPPm9H9mui&rqqW7KHJ0#VJ5YNMe9)^(5R&!&>NwI}gBP*+0hIY#e#C?o^*#7=hB$Sk2& z@Uz7xkQK_8>Hc7G%%M(kM>eJVLBqJdoVI$k%=HvujMr0Z+zPb0S!VO5zMDo;+w538 z?6@yZK>-sAA-R6VrDT{U$+)G674P}5_JA_U(_*o;U34Guc;$Q@ThShmSI&nWPh%_P ze*39>(RkV`%dL%HSad4o$0r_O98A3oO_y4qgigpxWl_vS1({n*c78)r)Y`tq43?J` zGc)i2?Sas|-fv+nJpq;0ACrz1Mjj6Y*R#8dMgU?s*O$doK{j}U4T4o5T6o?Sxf@= zvV+~`dh3pO8h&5EPIeGIl$cXjC(|M3tin-M(hzaP%{sXc#IntN#2x)MRhy^axs4E4 z5#*q;(ZX2w%Sw?WnY6je_GaH-Q3j%`7r9T1Y*(oN$q+IX&=PJ#toHn1ecF` zzs;@~>K(P_T$H~M^?N@4944|~;{J8y`*l+Jw^TIH3+46J$xKk#HGtoV{&@!C^CXp4 zD~Celdn$d1{rK8?^2P~|f|M`O7tqz#-h=_;ZhK#;_cr&F5AL9 zU4PMG5(DI>%a7=qnfD}^R^&yG$6Er{KOtU4Eb~6ECgwxg+-0E<_B?6pnY!kPKi=W6 zJJpLzc-vdB#LA)s-F%hKi0nJkz&u00k2^{Yq1=)IgNGQJth;C~k#{C%tF7ZWp4egz% zZT|&QM^)k#QhU~;+2#09yWUIU?|7^T;+={|$ zEOov*h!pSRxx00tZA#S5wqAf`ss@C0s|^Z4DDOf2Qgq8u;#z};YO|x-!=SjLuGMJ+ zEOAh<8_z)0>+=nkKY_-a=^N1KwN();!fG?Rl`>HHwconxqkN^a2oj+9~Mt zEi8~~5&*~Af(c~}`YrLDYEm|$-J9{wt+_M_aaK?Zl(mVCAt7@$;@x3LQ^|QFs7rvN zrW0mYV{uC=4$)7}QVU(O8->i(WS5*FG&>*d8i8+WbW$IU!UK~jw%cr=nDlUi#AHVK zcn6#EKR0}oCZmG4jiMB1%~qlv*-uq&VVR?dBWL``-^+b5s1O zU9rUlRZmFQM#n|z-h^PsCf@B*xpq_E5g*R;{`x3YH(ONM!k*6C5Fet4e?2GlZ2F1; zdX=xCk$0l`^?F2{P*^BBVbM4boWw=w_s>S<>1yLa$MT^ahZ*(Rk@-4r49KFvf1oFW zvR3?ML@t!7klQO7=^e?we`fT;q9NYpthWJ&@HgVwQWrlMl_i==Ys_y6UB3VvMeV43 z{YR?&dh1GjdPhLP_}r-2qEx)|K8S!rY=iDB&gwFwmN&{9Y@5cS%tuD$i2t$EoiB$T z20vu^{B-fqbWsZt;2`|Fqda`|Ki>$N{moG98#8{}c& zY)%FTQ(u(Z6|R;S(tYGLkb@YP}m1auI%(87`-Ke2r6MI7+(QFlgnW65~v+jwiJihA>q zD2CDImB#zCXA5%$EPQERbE4O0M22SxrTnji_-08OYMY_@;-LZM16AE9_qWWb5tfmv zF+-f-eH*L8`>(GtSZA3X>)wBzI92bHjRQ6AIGo?2e!z^;9;FLP_rv6{t=>o4kI`2U z_)zLVcq=gIw+tU1TYJP-3P}xM$CK6uX-&Ll2ZKH_!+@o3#j|$YDiV{;HGF63rsh#r z4;iH(*zr8umcuj=qy4&`2KKvHqpko@nx+jyDHHMe7kAZ8KnEnJ36n-_U1wHA%OiAY z@5A?gKc2MZaTq&u?XY7l(IF<{b>GDKWL`k3-eKrah}$5h^6OpA(r2K&^Fcrp+pnN* zC!s%nf!^p+VhZ^)9KVJrsoV;@I{BbCy$oif%Zz-rzZ*B4O~O}V!n$~?PL|q_*}L4d zZS{@~P9I&t-sMr#I*8MZbV`Q>=ms1&hJ9frl0f8|z~1HP)C3{3g?1{Qs2DyBV`6=`@AC%TXp1`^2j7ux%ZKdE^;e zlYft=v4bD4Mwf>CL!w!d{wOv572%OEW$(L~6q2!!SJWK{b+{c+PUff@tGRwFP=02ELx|#`J(vT@PNhjvL7m0fBB3AKR23X0JjQ5`W`x1PDZiky zYLsWtP>+l@4bS)TmQl>D92T{$Y-po46%QAyS$sO{=hhqyEUzK-kMV2;E6O`HIsr;S zVYaotHg;@I3oG9-i5-dANfzci%O;@Uo~|3TFSwsg;AWM+vAkB!kz@@srM@KY>{?MT zf4r$)ey=ku*&tGP;uLKN^-)AoCHIXU;+bDsYyBPq;UBvDAe53I_M!<>GJgvpQVKE3 zL3&QiC49TcO3~gGGbY zPTyo1ysXwd3tZ0mP;`DiGFShpDyz}@BRGL0F>}9AoUh-)Y323uaR|A?3@L8M$Eaco zu?Sb*q!iY9hAply93tLh6S`s3gP0EtzfC`7Q~kzjxxZo1wakG*4B>t4VFfZzvNa_x zu1DH6P8-Z~q%AO`%Z0@ShMLhKUR{R)!0i6NM&*e&SbXzm(OuwXLC$pgUz zpDh}wV`Vj6GkLNw(bnV2>uIh>qcRNaL zFnpPB7p04JY>LhpMrS|`oMM^u%;*Bq#O?!nl)lG2Y_Qq$&uV|Df zK{PW`r2b*Sa8b?g)gp8#NnIA@k=oV>7Pz7infnT9Y#v!)DEAA9e-mWHER0C&<8cLk z<=tLV8#w^)^LJRZU_zd#RDXL|9x-0)MNnTfW<=MLprSnS=u4kQ^I!m&i|^o8>cddR z2_jKkp#M=w`#eyB(jDY&#~*7szS?_)6lY3Ho9K;-;-Xw_LQ#%3wJ=9a^h884KC1~B zNauplfsTT<8x{kDg0fSETIsGTAsyLzkA^UU;Nlw4=JxnvK=A%;xNe7tkrB^kOPlw5 zhn2gFwS_K*Eh*`+G{m;~A)B4V_{cB=JS|osO<6J zMNV9}&{0ximKX->+1H_KIwkR5i>84vej8S*qvJ`i0v!#L{28DF%0o9Lg`FOu>a~8v3MN)1MnVA@}w!EIjFc7mt zoWCDsLJYeb$31$g(+0~+2k@d_VaR*8`Xs#GgKS-U#1itZ_%|U8`@1ZPNz1srt6}=Z zEQWb?6&SIHQ^CiS9 zL-X9uE*U9;lv_b5yL1ZUMYYH`Kwk(XrVFDUf6E#>0!v~a;tLKptOYNtr9&*T#%cz6 zcoCo>?2Ve&vuSub^c1^530biQ{^%6V5I@K5#N?OOwAl2|4^0}LVOb$PJm!k8<@EZ( zwLKXzr!Y$P!H8QZlEN%#uWTUCoN>x=EE>;Kitnu$-(fu8%iuVWM@L?3Zc@kx_FZXi zCNX9Vg?OD9rzyukQZa@)IgJU<`0-8 zUs73_lIIyRO9x$w81&xQ9MQt{@*38mn0V|_`NoU!`qs3|A)&B!v>7!!AG_E0GOt_P zR1ul#@oAGmysAg#c@+1ysWy5f%8s-PgVO&8(j-4d#{%vbUc(nQoNy^HQar|g z8&+uJD>)VeDhSR}=}aqYvPXS4+8kujX=mAc&OB#u>ykzuHb}>M^>gdN1RWs23 z*9!BtUJyHfg*--@_7Nu@p#Prw5Q6#v`~mMX%{mJf9v16{#$Wp6Mm`7_3(JD3!7WD-J9r?0gp$_C*Ah8aP_zvr*n8 zXMsEU2DpIDC||AlK|M}~rgR3lr^UrXjD!<^N2l7Vo`DqYfgx$4hAk=_`U8wzIGRKEjus+RS{s?B3H=z&&}xgaodLokB)FkT zQfMr;DUk=F;kPhApnr)j9Tn4guvkf~$M)!N4$l^wx3tOpNZ8^4XmV7ZkuXqfstHdq zH1B>;$5xEY@hBy0X-)JnVHkx{MphK(`8b?}sA`~NEor5IGx(SdElATV>Yy(He*+%< zEw+HMz64_8L|)Jvx5V)RToPuXMY6Rx-}{O4PB}&QfLqn5I_?c#)+}d$oa#^pmkt>s zYFNzlW|88^+9a)=T;TX3u?P3eqau@}$7_$wp*1|gFwqo#8UfMBoB+!f=i|wtJsQfh zHLG)UEY=aPRnt-^wYH4frcHBV9WjwN!{BDxsVQy}=QVPHRI=ZHm6}gGO<1ZfUP@^T zP&8f-_GnM3BE7SdthjeMPhRh=rhKCry0XK%#Ie3WFYQmUbFlfZFw1z*YYG0K{|44c zHS~ymbP7h$z_+?WM|{<=z5-H;ko!XyHWhw)^{8_4%pa0&@43uLrks_?DkncLNVaW98&g_&Np zKNNy~gS1EZh>J0*ZW_cDA^#;U{fb0!x_%9Kw5Nn^DA8@vA_!)%PNCM%gRsovv~>-^ zp-PmI@|Cd136L&T#7FtMw5ZF|UZbD0cR6IHL>mmvx`Ra-VNsW;WZJKBIM<+VsrX9% zY|7(BzkFnPou~keLym%#v~qAn@) z7<-rJ6xmnuEfX>smY?vVUn7&g&1q}uI)Ib5(@3UsN~9$H|b-V=(x z?gKqoaG0aa8=X8@#)3P+J?mt&xM)j@>bMBt@BZ$a-R0EY@vVaZC%`MZICX7Mdx>xy|s_K z&(yAj2=%9wc9HNBmWDXejzVb(omRYE5TQo%HAFXM6&jJ}{iw227V~vsiX(d2?eu6k z5Pm)2@7fBbBc#pq`RFbq$+I>Z6{mqqDG;6Bgcr6gt(mB~5MC!nP=_>@W(dbTf1Sg% zD;0pDX@3ox=5qnf=yFqtic4(6{If?GYCRfHfTa!cG>y!3oeU*rXib57JJXsA5lZMm zC+3pRr@s^mS>KSR?1=ZBA?N{<>~{hlJ+ZjbvUCj2NU;>&M5tO}$#aAzQ=wp1q7$rM zwWSUA*({=Uw6`TZ-!pZpYK;oD6{ho9&5jLZeqf zEKP&7gxCeW<0Ig#mrK?M^)dQI?Ajj6m^ym{&%C4xEU5IwdMz`{&!&-JyaL;c42IH2#8$s z*uh^|+P_#Q!66oW(&m}n_C&QiSl1F;82UYsj<#?!{ngpwLLLS(cQizpYKSyy#;k0V zr$jB9s7qQI51GnfT>z-sSY4+jBXfijS0MiVkXOIsc$J;~SeFy^(CN6>pbdU9A0i?| zjv*M3Cy{2sqNG2fR!BqtAT(ZTMjU6jm>N@1`PzN1gOEU^9&cZl>W^vLKW|3vd`goYvjrEa( zge?LvTW=iUSaOVt2AXcc^i&TE5Qi`2Hb6h47SWE#r6pWnj%Jvm`-*Jxw4D@w)L^Ip~#a5Z%P`mnD@l zWvFyGo1!8ov?(vt_2)WVVa&%)>A1WBN`V$D91k1Iq7-|zY*|O0GiErh1|asiR$7Za zDl1FXL+NfxDI+6;?Dzs-*6)q6$eWr9u`y=FS|X(fmIe=A7LA8kf=u2{8wjv$NQ;1LfgVzSo+ShZSa9aaCAX+f6e+lnd7V8a#Wo z-|r}ZEoo1K4(ED&nU{2x#a_YkM*Ow6_EgpEAE*$nc2{Ut)b(n%?wFdb7H^s<~L%)rNNPcD9?NSsW6&{!Dzw2vrwhpD9Rx!_bYyf>w_-9%2VO%+^Z8tf-(2 z)PFuZ*%j28YJq4-qkWWR2gjyOq%4nKPug5|u(LIpShu8+|5A_P`Aka-+?OcdroVGY z*VYDG^(T8zM=V+twPZs5QPD>N?8zUsEwRCzq@pp*W3$2K(%P+Q7++_C9MmZaVnbBBY7gZK8j z6%D!CPiPNE%)*nOoHk~dmswX&a$0e|@gPnQCZ{caQ~ST~ewSbo>=J(>xnctEr&Amg zub3mi44DDKrj5yUM1+kiY4NxNMJ|WcDav!A35s0pA67m67Z#d?7z=an6=nzh35v?r zttxG67elx4TWBhi7DTm`1DY^TH>F5{Nl9@o=&;T*5{eLw%p*;~;(YX^;Y%@8C|OUG z%4LdBx*xXe@flp;Vmx~;`Yz!PqS@LrAogpa6Y2`UF6pn(lRf9r9E%t!PXlWnNj(!6 zPJO+BW}imCfnh+SwyyoTz-`s&mbjY0)`r`h1=E($m+R8LPO|GtY|*GG%>|m^e_fV6 z5a|c--w0he%Og-kP}juYAWEfM5nD+KmPK_SVk(w&N~XiTz{apnlluLd*7dhGE%`A- z(|_RawJM$_q<8}yay9-SpVQuq<8P7$$vP1iE?l^9;lhOr7cN}5aN)v*3l}b2xNzaZ pg$oxhT)1%I!i5VLE?jJd{|B^CnZ;RP{bT?D002ovPDHLkV1o5K^