From 712f219c7cccf4a3a96217da119ef23331b95edf Mon Sep 17 00:00:00 2001 From: Mathew May Date: Thu, 24 Nov 2022 16:08:39 +0800 Subject: [PATCH] MDL-76134 gradebook: Hook into tertiary dropdown item --- grade/amd/build/searchwidget/initials.min.js | 10 ++ .../build/searchwidget/initials.min.js.map | 1 + grade/amd/src/searchwidget/initials.js | 169 ++++++++++++++++++ grade/renderer.php | 48 +++++ .../grader/classes/output/action_bar.php | 28 ++- grade/report/grader/index.php | 23 +-- grade/report/grader/styles.css | 9 + .../grader/templates/action_bar.mustache | 6 + .../templates/initials_dropdown_form.mustache | 38 ++++ lang/en/grades.php | 4 + lang/en/moodle.php | 1 + 11 files changed, 320 insertions(+), 17 deletions(-) create mode 100644 grade/amd/build/searchwidget/initials.min.js create mode 100644 grade/amd/build/searchwidget/initials.min.js.map create mode 100644 grade/amd/src/searchwidget/initials.js create mode 100644 grade/templates/initials_dropdown_form.mustache diff --git a/grade/amd/build/searchwidget/initials.min.js b/grade/amd/build/searchwidget/initials.min.js new file mode 100644 index 00000000000..0d68127de2e --- /dev/null +++ b/grade/amd/build/searchwidget/initials.min.js @@ -0,0 +1,10 @@ +define("core_grades/searchwidget/initials",["exports","core/pending","core/url","core/custom_interaction_events","jquery"],(function(_exports,_pending,Url,_custom_interaction_events,_jquery){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * A small dropdown to filter users within the gradebook. + * + * @module core_grades/searchwidget/initials + * @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),Url=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(Url),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_jquery=_interopRequireDefault(_jquery);let registered=!1;const selectors_pageListItem="page-item",selectors_pageClickableItem=".page-link",selectors_activeItem="active",selectors_formDropdown=".initialsdropdownform",selectors_parentDomNode=".initials-selector",selectors_firstInitial="firstinitial",selectors_lastInitial="lastinitial",selectors_initialBars=".initialbar",selectors_targetButton="initialswidget",selectors_formItems={type:"submit",save:"save",cancel:"cancel"};_exports.init=callingLink=>{if(registered)return;const pendingPromise=new _pending.default;registerListenerEvents(callingLink),(0,_jquery.default)(selectors_parentDomNode).on("shown.bs.dropdown",(()=>{document.querySelector(selectors_pageClickableItem).focus({preventScroll:!0})})),pendingPromise.resolve(),registered=!0};const registerListenerEvents=callingLink=>{const events=["click",_custom_interaction_events.default.events.activate,_custom_interaction_events.default.events.keyboardActivate];_custom_interaction_events.default.define(document,events),events.forEach((event=>{document.addEventListener(event,(e=>{let{firstActive:firstActive,lastActive:lastActive,sifirst:sifirst,silast:silast}=onClickVariables(),itemToReset="";if(e.target.closest(selectors_formDropdown)&&e.preventDefault(),e.target.closest("".concat(selectors_formDropdown," .").concat(selectors_pageListItem))){if(e.target.classList.contains(selectors_pageListItem))return;e.target.closest(selectors_initialBars).classList.contains(selectors_firstInitial)?(sifirst=e.target,itemToReset=firstActive):(silast=e.target,itemToReset=lastActive),swapActiveItems(itemToReset,e)}e.target.type===selectors_formItems.type&&(e.target.dataset.action===selectors_formItems.save&&(window.location=Url.relativeUrl(callingLink,{id:e.target.closest(selectors_formDropdown).dataset.courseid,sifirst:sifirst.parentElement.classList.contains("initialbarall")?"":sifirst.value,silast:silast.parentElement.classList.contains("initialbarall")?"":silast.value})),e.target.dataset.action===selectors_formItems.cancel&&(0,_jquery.default)(".".concat(selectors_targetButton)).dropdown("toggle"))}))}))},onClickVariables=()=>{const firstItems=[...document.querySelectorAll(".".concat(selectors_firstInitial," li"))],lastItems=[...document.querySelectorAll(".".concat(selectors_lastInitial," li"))],firstActive=firstItems.filter((item=>item.classList.contains(selectors_activeItem)))[0],lastActive=lastItems.filter((item=>item.classList.contains(selectors_activeItem)))[0];let sifirst=firstActive.querySelector(selectors_pageClickableItem),silast=lastActive.querySelector(selectors_pageClickableItem);return{firstActive:firstActive,lastActive:lastActive,sifirst:sifirst,silast:silast}},swapActiveItems=(itemToReset,e)=>{itemToReset.classList.remove(selectors_activeItem),itemToReset.querySelector(selectors_pageClickableItem).ariaCurrent=!1;e.target.parentElement.classList.add(selectors_activeItem),e.target.ariaCurrent=!0}})); + +//# sourceMappingURL=initials.min.js.map \ No newline at end of file diff --git a/grade/amd/build/searchwidget/initials.min.js.map b/grade/amd/build/searchwidget/initials.min.js.map new file mode 100644 index 00000000000..5ef01f43f74 --- /dev/null +++ b/grade/amd/build/searchwidget/initials.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"initials.min.js","sources":["../../src/searchwidget/initials.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 dropdown to filter users within the gradebook.\n *\n * @module core_grades/searchwidget/initials\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 Url from 'core/url';\nimport CustomEvents from \"core/custom_interaction_events\";\nimport $ from 'jquery';\n\n/**\n * Whether the event listener has already been registered for this module.\n *\n * @type {boolean}\n */\nlet registered = false;\n\n// Contain our selectors within this file until they could be of use elsewhere.\nconst selectors = {\n pageListItem: 'page-item',\n pageClickableItem: '.page-link',\n activeItem: 'active',\n formDropdown: '.initialsdropdownform',\n parentDomNode: '.initials-selector',\n firstInitial: 'firstinitial',\n lastInitial: 'lastinitial',\n initialBars: '.initialbar', // Both first and last name use this class.\n targetButton: 'initialswidget',\n formItems: {\n type: 'submit',\n save: 'save',\n cancel: 'cancel'\n }\n};\n\n/**\n * Our initial hook into the module which will eventually allow us to handle the dropdown initials bar form.\n *\n * @param {String} callingLink The link to redirect upon form submission.\n */\nexport const init = (callingLink) => {\n if (registered) {\n return;\n }\n const pendingPromise = new Pending();\n registerListenerEvents(callingLink);\n // BS events always bubble so, we need to listen for the event higher up the chain.\n $(selectors.parentDomNode).on('shown.bs.dropdown', () => {\n document.querySelector(selectors.pageClickableItem).focus({preventScroll: true});\n });\n pendingPromise.resolve();\n registered = true;\n};\n\n/**\n * Register event listeners.\n *\n * @param {String} callingLink The link to redirect upon form submission.\n */\nconst registerListenerEvents = (callingLink) => {\n const events = [\n 'click',\n CustomEvents.events.activate,\n CustomEvents.events.keyboardActivate\n ];\n CustomEvents.define(document, events);\n\n // Register events.\n events.forEach((event) => {\n document.addEventListener(event, (e) => {\n // Always fetch the latest information when we click as state is a fickle thing.\n let {firstActive, lastActive, sifirst, silast} = onClickVariables();\n let itemToReset = '';\n\n // Prevent the usual form behaviour.\n if (e.target.closest(selectors.formDropdown)) {\n e.preventDefault();\n }\n\n // Handle the state of active initials before form submission.\n if (e.target.closest(`${selectors.formDropdown} .${selectors.pageListItem}`)) {\n // Ensure the li items don't cause weird clicking emptying out the form.\n if (e.target.classList.contains(selectors.pageListItem)) {\n return;\n }\n\n const initialsBar = e.target.closest(selectors.initialBars); // Find out which initial bar we are in.\n\n // We want to find the current active item in the menu area the user selected.\n // We also want to fetch the raw item out of the array for instant manipulation.\n if (initialsBar.classList.contains(selectors.firstInitial)) {\n sifirst = e.target;\n itemToReset = firstActive;\n } else {\n silast = e.target;\n itemToReset = lastActive;\n }\n swapActiveItems(itemToReset, e);\n }\n\n // Handle form submissions.\n if (e.target.type === selectors.formItems.type) {\n if (e.target.dataset.action === selectors.formItems.save) {\n // Ensure we strip out the value (All) as it messes with the PHP side of the initials bar.\n // Then we will redirect the user back onto the page with new filters applied.\n window.location = Url.relativeUrl(callingLink, {\n 'id': e.target.closest(selectors.formDropdown).dataset.courseid,\n 'sifirst': sifirst.parentElement.classList.contains('initialbarall') ? '' : sifirst.value,\n 'silast': silast.parentElement.classList.contains('initialbarall') ? '' : silast.value,\n });\n }\n if (e.target.dataset.action === selectors.formItems.cancel) {\n $(`.${selectors.targetButton}`).dropdown('toggle');\n }\n }\n });\n });\n};\n\n/**\n * A small abstracted helper function which allows us to ensure we have up-to-date lists of nodes.\n *\n * @returns {{firstActive: HTMLElement, lastActive: HTMLElement, sifirst: ?String, silast: ?String}}\n */\nconst onClickVariables = () => {\n // Ensure we have an up-to-date initials bar.\n const firstItems = [...document.querySelectorAll(`.${selectors.firstInitial} li`)];\n const lastItems = [...document.querySelectorAll(`.${selectors.lastInitial} li`)];\n const firstActive = firstItems.filter((item) => item.classList.contains(selectors.activeItem))[0];\n const lastActive = lastItems.filter((item) => item.classList.contains(selectors.activeItem))[0];\n // Ensure we retain both of the selections from a previous instance.\n let sifirst = firstActive.querySelector(selectors.pageClickableItem);\n let silast = lastActive.querySelector(selectors.pageClickableItem);\n return {firstActive, lastActive, sifirst, silast};\n};\n\n/**\n * Given we are provided the old li and current click event, swap around the active properties.\n *\n * @param {HTMLElement} itemToReset\n * @param {Event} e\n */\nconst swapActiveItems = (itemToReset, e) => {\n itemToReset.classList.remove(selectors.activeItem);\n itemToReset.querySelector(selectors.pageClickableItem).ariaCurrent = false;\n\n // Set the select item as the current item.\n const itemToSetActive = e.target.parentElement;\n itemToSetActive.classList.add(selectors.activeItem);\n e.target.ariaCurrent = true;\n};\n"],"names":["registered","selectors","type","save","cancel","callingLink","pendingPromise","Pending","registerListenerEvents","on","document","querySelector","focus","preventScroll","resolve","events","CustomEvents","activate","keyboardActivate","define","forEach","event","addEventListener","e","firstActive","lastActive","sifirst","silast","onClickVariables","itemToReset","target","closest","preventDefault","classList","contains","swapActiveItems","dataset","action","window","location","Url","relativeUrl","courseid","parentElement","value","dropdown","firstItems","querySelectorAll","lastItems","filter","item","remove","ariaCurrent","add"],"mappings":";;;;;;;44BAiCIA,YAAa,QAGXC,uBACY,YADZA,4BAEiB,aAFjBA,qBAGU,SAHVA,uBAIY,wBAJZA,wBAKa,qBALbA,uBAMY,eANZA,sBAOW,cAPXA,sBAQW,cARXA,uBASY,iBATZA,oBAUS,CACPC,KAAM,SACNC,KAAM,OACNC,OAAQ,wBASKC,iBACbL,wBAGEM,eAAiB,IAAIC,iBAC3BC,uBAAuBH,iCAErBJ,yBAAyBQ,GAAG,qBAAqB,KAC/CC,SAASC,cAAcV,6BAA6BW,MAAM,CAACC,eAAe,OAE9EP,eAAeQ,UACfd,YAAa,SAQXQ,uBAA0BH,oBACtBU,OAAS,CACX,QACAC,mCAAaD,OAAOE,SACpBD,mCAAaD,OAAOG,qDAEXC,OAAOT,SAAUK,QAG9BA,OAAOK,SAASC,QACZX,SAASY,iBAAiBD,OAAQE,QAE1BC,YAACA,YAADC,WAAcA,WAAdC,QAA0BA,QAA1BC,OAAmCA,QAAUC,mBAC7CC,YAAc,MAGdN,EAAEO,OAAOC,QAAQ9B,yBACjBsB,EAAES,iBAIFT,EAAEO,OAAOC,kBAAW9B,oCAA2BA,yBAA2B,IAEtEsB,EAAEO,OAAOG,UAAUC,SAASjC,+BAIZsB,EAAEO,OAAOC,QAAQ9B,uBAIrBgC,UAAUC,SAASjC,yBAC/ByB,QAAUH,EAAEO,OACZD,YAAcL,cAEdG,OAASJ,EAAEO,OACXD,YAAcJ,YAElBU,gBAAgBN,YAAaN,GAI7BA,EAAEO,OAAO5B,OAASD,oBAAoBC,OAClCqB,EAAEO,OAAOM,QAAQC,SAAWpC,oBAAoBE,OAGhDmC,OAAOC,SAAWC,IAAIC,YAAYpC,YAAa,IACrCkB,EAAEO,OAAOC,QAAQ9B,wBAAwBmC,QAAQM,iBAC5ChB,QAAQiB,cAAcV,UAAUC,SAAS,iBAAmB,GAAKR,QAAQkB,aAC1EjB,OAAOgB,cAAcV,UAAUC,SAAS,iBAAmB,GAAKP,OAAOiB,SAGrFrB,EAAEO,OAAOM,QAAQC,SAAWpC,oBAAoBG,uCAC1CH,yBAA0B4C,SAAS,kBAYvDjB,iBAAmB,WAEfkB,WAAa,IAAIpC,SAASqC,4BAAqB9C,gCAC/C+C,UAAY,IAAItC,SAASqC,4BAAqB9C,+BAC9CuB,YAAcsB,WAAWG,QAAQC,MAASA,KAAKjB,UAAUC,SAASjC,wBAAuB,GACzFwB,WAAauB,UAAUC,QAAQC,MAASA,KAAKjB,UAAUC,SAASjC,wBAAuB,OAEzFyB,QAAUF,YAAYb,cAAcV,6BACpC0B,OAASF,WAAWd,cAAcV,mCAC/B,CAACuB,YAAAA,YAAaC,WAAAA,WAAYC,QAAAA,QAASC,OAAAA,SASxCQ,gBAAkB,CAACN,YAAaN,KAClCM,YAAYI,UAAUkB,OAAOlD,sBAC7B4B,YAAYlB,cAAcV,6BAA6BmD,aAAc,EAG7C7B,EAAEO,OAAOa,cACjBV,UAAUoB,IAAIpD,sBAC9BsB,EAAEO,OAAOsB,aAAc"} \ No newline at end of file diff --git a/grade/amd/src/searchwidget/initials.js b/grade/amd/src/searchwidget/initials.js new file mode 100644 index 00000000000..bc7ed37246b --- /dev/null +++ b/grade/amd/src/searchwidget/initials.js @@ -0,0 +1,169 @@ +// 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 dropdown to filter users within the gradebook. + * + * @module core_grades/searchwidget/initials + * @copyright 2022 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Pending from 'core/pending'; +import * as Url from 'core/url'; +import CustomEvents from "core/custom_interaction_events"; +import $ from 'jquery'; + +/** + * Whether the event listener has already been registered for this module. + * + * @type {boolean} + */ +let registered = false; + +// Contain our selectors within this file until they could be of use elsewhere. +const selectors = { + pageListItem: 'page-item', + pageClickableItem: '.page-link', + activeItem: 'active', + formDropdown: '.initialsdropdownform', + parentDomNode: '.initials-selector', + firstInitial: 'firstinitial', + lastInitial: 'lastinitial', + initialBars: '.initialbar', // Both first and last name use this class. + targetButton: 'initialswidget', + formItems: { + type: 'submit', + save: 'save', + cancel: 'cancel' + } +}; + +/** + * Our initial hook into the module which will eventually allow us to handle the dropdown initials bar form. + * + * @param {String} callingLink The link to redirect upon form submission. + */ +export const init = (callingLink) => { + if (registered) { + return; + } + const pendingPromise = new Pending(); + registerListenerEvents(callingLink); + // BS events always bubble so, we need to listen for the event higher up the chain. + $(selectors.parentDomNode).on('shown.bs.dropdown', () => { + document.querySelector(selectors.pageClickableItem).focus({preventScroll: true}); + }); + pendingPromise.resolve(); + registered = true; +}; + +/** + * Register event listeners. + * + * @param {String} callingLink The link to redirect upon form submission. + */ +const registerListenerEvents = (callingLink) => { + const events = [ + 'click', + CustomEvents.events.activate, + CustomEvents.events.keyboardActivate + ]; + CustomEvents.define(document, events); + + // Register events. + events.forEach((event) => { + document.addEventListener(event, (e) => { + // Always fetch the latest information when we click as state is a fickle thing. + let {firstActive, lastActive, sifirst, silast} = onClickVariables(); + let itemToReset = ''; + + // Prevent the usual form behaviour. + if (e.target.closest(selectors.formDropdown)) { + e.preventDefault(); + } + + // Handle the state of active initials before form submission. + if (e.target.closest(`${selectors.formDropdown} .${selectors.pageListItem}`)) { + // Ensure the li items don't cause weird clicking emptying out the form. + if (e.target.classList.contains(selectors.pageListItem)) { + return; + } + + const initialsBar = e.target.closest(selectors.initialBars); // Find out which initial bar we are in. + + // We want to find the current active item in the menu area the user selected. + // We also want to fetch the raw item out of the array for instant manipulation. + if (initialsBar.classList.contains(selectors.firstInitial)) { + sifirst = e.target; + itemToReset = firstActive; + } else { + silast = e.target; + itemToReset = lastActive; + } + swapActiveItems(itemToReset, e); + } + + // Handle form submissions. + if (e.target.type === selectors.formItems.type) { + if (e.target.dataset.action === selectors.formItems.save) { + // Ensure we strip out the value (All) as it messes with the PHP side of the initials bar. + // Then we will redirect the user back onto the page with new filters applied. + window.location = Url.relativeUrl(callingLink, { + 'id': e.target.closest(selectors.formDropdown).dataset.courseid, + 'sifirst': sifirst.parentElement.classList.contains('initialbarall') ? '' : sifirst.value, + 'silast': silast.parentElement.classList.contains('initialbarall') ? '' : silast.value, + }); + } + if (e.target.dataset.action === selectors.formItems.cancel) { + $(`.${selectors.targetButton}`).dropdown('toggle'); + } + } + }); + }); +}; + +/** + * A small abstracted helper function which allows us to ensure we have up-to-date lists of nodes. + * + * @returns {{firstActive: HTMLElement, lastActive: HTMLElement, sifirst: ?String, silast: ?String}} + */ +const onClickVariables = () => { + // Ensure we have an up-to-date initials bar. + const firstItems = [...document.querySelectorAll(`.${selectors.firstInitial} li`)]; + const lastItems = [...document.querySelectorAll(`.${selectors.lastInitial} li`)]; + const firstActive = firstItems.filter((item) => item.classList.contains(selectors.activeItem))[0]; + const lastActive = lastItems.filter((item) => item.classList.contains(selectors.activeItem))[0]; + // Ensure we retain both of the selections from a previous instance. + let sifirst = firstActive.querySelector(selectors.pageClickableItem); + let silast = lastActive.querySelector(selectors.pageClickableItem); + return {firstActive, lastActive, sifirst, silast}; +}; + +/** + * Given we are provided the old li and current click event, swap around the active properties. + * + * @param {HTMLElement} itemToReset + * @param {Event} e + */ +const swapActiveItems = (itemToReset, e) => { + itemToReset.classList.remove(selectors.activeItem); + itemToReset.querySelector(selectors.pageClickableItem).ariaCurrent = false; + + // Set the select item as the current item. + const itemToSetActive = e.target.parentElement; + itemToSetActive.classList.add(selectors.activeItem); + e.target.ariaCurrent = true; +}; diff --git a/grade/renderer.php b/grade/renderer.php index 49aba778eb4..11962176497 100644 --- a/grade/renderer.php +++ b/grade/renderer.php @@ -85,6 +85,54 @@ class core_grades_renderer extends plugin_renderer_base { return $this->render_from_template('core_grades/group_selector', $data); } + /** + * Build the data to render the initials bar filter within the gradebook. + * Using this initials selector means you'll have to retain the use of the templates & JS to handle form submission. + * If a simple redirect on each selection is desired the standard user_search() within the user renderer is what you are after. + * + * @param object $course The course object. + * @param context $context Our current context. + * @param string $slug The slug for the report that called this function. + * @return stdClass The data to output. + */ + public function initials_selector( + object $course, + context $context, + string $slug + ): stdClass { + global $SESSION, $COURSE; + // User search. + $url = new moodle_url($slug, ['id' => $course->id]); + $firstinitial = $SESSION->gradereport["filterfirstname-{$context->id}"] ?? ''; + $lastinitial = $SESSION->gradereport["filtersurname-{$context->id}"] ?? ''; + + $renderer = $this->page->get_renderer('core_user'); + $initialsbar = $renderer->partial_user_search($url, $firstinitial, $lastinitial, true); + + $currentfilter = ''; + if ($firstinitial !== '' && $lastinitial !== '') { + $currentfilter = get_string('filterbothactive', 'grades', ['first' => $firstinitial, 'last' => $lastinitial]); + } else if ($firstinitial !== '') { + $currentfilter = get_string('filterfirstactive', 'grades', ['first' => $firstinitial]); + } else if ($lastinitial !== '') { + $currentfilter = get_string('filterlastactive', 'grades', ['last' => $lastinitial]); + } + + $this->page->requires->js_call_amd('core_grades/searchwidget/initials', 'init', [$slug]); + + $formdata = (object) [ + 'courseid' => $COURSE->id, + 'initialsbars' => $initialsbar, + ]; + $dropdowncontent = $this->render_from_template('core_grades/initials_dropdown_form', $formdata); + + return (object) [ + 'buttoncontent' => $currentfilter !== '' ? $currentfilter : get_string('filterbyname', 'core_grades'), + 'buttonheader' => $currentfilter !== '' ? get_string('name') : null, + 'dropdowncontent' => $dropdowncontent, + ]; + } + /** * Creates and renders a custom user heading. * diff --git a/grade/report/grader/classes/output/action_bar.php b/grade/report/grader/classes/output/action_bar.php index dd100ecc87d..631404083b3 100644 --- a/grade/report/grader/classes/output/action_bar.php +++ b/grade/report/grader/classes/output/action_bar.php @@ -18,6 +18,7 @@ namespace gradereport_grader\output; use moodle_url; use core_grades\output\general_action_bar; +use core_grades\output\gradebook_dropdown; /** * Renderable class for the action bar elements in the grader report. @@ -53,12 +54,37 @@ class action_bar extends \core_grades\output\action_bar { * @return array */ public function export_for_template(\renderer_base $output): array { + global $PAGE; // If in the course context, we should display the general navigation selector in gradebook. $courseid = $this->context->instanceid; // Get the data used to output the general navigation selector. $generalnavselector = new general_action_bar($this->context, new moodle_url('/grade/report/grader/index.php', ['id' => $courseid]), 'gradereport', 'grader'); - return $generalnavselector->export_for_template($output); + $data = $generalnavselector->export_for_template($output); + + // If the user has the capability to view all grades, display the group selector (if applicable), the user selector + // and the view mode selector (if applicable). + if (has_capability('moodle/grade:viewall', $this->context)) { + $course = get_course($courseid); + $gradesrenderer = $PAGE->get_renderer('core_grades'); + + $initialscontent = $gradesrenderer->initials_selector( + $course, + $this->context, + '/grade/report/grader/index.php' + ); + $initialselector = new gradebook_dropdown( + false, + $initialscontent->buttoncontent, + $initialscontent->dropdowncontent, + 'initials-selector', + 'initialswidget', + 'initialsdropdown', + $initialscontent->buttonheader, + ); + $data['initialselector'] = $initialselector->export_for_template($output); + } + return $data; } } diff --git a/grade/report/grader/index.php b/grade/report/grader/index.php index 82dd64ad9a7..8a9b32feb33 100644 --- a/grade/report/grader/index.php +++ b/grade/report/grader/index.php @@ -109,19 +109,18 @@ if (!empty($target) && !empty($action) && confirm_sesskey()) { $reportname = get_string('pluginname', 'gradereport_grader'); -// Do this check just before printing the grade header (and only do it once). -grade_regrade_final_grades_if_required($course); - -$actionbar = new \gradereport_grader\output\action_bar($context); -// Print header -print_grade_page_head($COURSE->id, 'report', 'grader', $reportname, false, $buttons, true, - null, null, null, $actionbar); - //Initialise the grader report object that produces the table //the class grade_report_grader_ajax was removed as part of MDL-21562 $report = new grade_report_grader($courseid, $gpr, $context, $page, $sortitemid); $numusers = $report->get_numusers(true, true); +// Do this check just before printing the grade header (and only do it once). +grade_regrade_final_grades_if_required($course); + +$actionbar = new \gradereport_grader\output\action_bar($context, $report, $numusers); +print_grade_page_head($COURSE->id, 'report', 'grader', $reportname, false, $buttons, true, + null, null, null, $actionbar); + // make sure separate group does not prevent view if ($report->currentgroup == -2) { echo $OUTPUT->heading(get_string("notingroup")); @@ -141,14 +140,6 @@ $report->load_users(); $report->load_final_grades(); echo $report->group_selector; -// User search -$url = new moodle_url('/grade/report/grader/index.php', array('id' => $course->id)); -$firstinitial = $SESSION->gradereport["filterfirstname-{$context->id}"] ?? ''; -$lastinitial = $SESSION->gradereport["filtersurname-{$context->id}"] ?? ''; -$totalusers = $report->get_numusers(true, false); -$renderer = $PAGE->get_renderer('core_user'); -echo $renderer->user_search($url, $firstinitial, $lastinitial, $numusers, $totalusers, $report->currentgroupname); - //show warnings if any foreach ($warnings as $warning) { echo $OUTPUT->notification($warning); diff --git a/grade/report/grader/styles.css b/grade/report/grader/styles.css index 37ffc5407a5..83b866c4b5b 100644 --- a/grade/report/grader/styles.css +++ b/grade/report/grader/styles.css @@ -201,3 +201,12 @@ border: 1px solid #ccc; border-radius: 4px; } + +.path-grade-report-grader .initialsdropdown { + width: 460px; +} + +.path-grade-report-grader .initialsdropdown::after { + content: ''; +} + diff --git a/grade/report/grader/templates/action_bar.mustache b/grade/report/grader/templates/action_bar.mustache index 8697607e9be..cb3c8b0f3c5 100644 --- a/grade/report/grader/templates/action_bar.mustache +++ b/grade/report/grader/templates/action_bar.mustache @@ -69,5 +69,11 @@ {{/generalnavselector}} + {{#initialselector}} + + + {{/initialselector}} diff --git a/grade/templates/initials_dropdown_form.mustache b/grade/templates/initials_dropdown_form.mustache new file mode 100644 index 00000000000..a363213fc6e --- /dev/null +++ b/grade/templates/initials_dropdown_form.mustache @@ -0,0 +1,38 @@ +{{! + 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/initials_dropdown_form + + The form component of our dropdown within the tertiary navigation. + + Context variables required for this template: + * courseid - The course ID. + * initialsbars - The rendered mini renderer from the user renderer. + + Example context (json): + { + "courseid": "2", + "initialsbars": "Output from renderer method" + } +}} +
+ {{{initialsbars}}} +
+ + +
+
diff --git a/lang/en/grades.php b/lang/en/grades.php index 0e006ea8c7b..2b8fba1ccda 100644 --- a/lang/en/grades.php +++ b/lang/en/grades.php @@ -239,6 +239,10 @@ $string['feedbacks'] = 'Feedbacks'; $string['feedbacksaved'] = 'Feedback saved'; $string['feedbackview'] = 'View feedback'; $string['finalgrade'] = 'Final grade'; +$string['filterbothactive'] = 'First ({$a->first}) Last ({$a->last})'; +$string['filterbyname'] = 'Filter by name'; +$string['filterfirstactive'] = 'First ({$a->first})'; +$string['filterlastactive'] = 'Last ({$a->last})'; $string['finalgrade_help'] = 'If the overridden checkbox is ticked, a grade may be added or amended.'; $string['fixedstudents'] = 'Static students column'; $string['fixedstudents_help'] = 'Allows grades to scroll horizontally without losing sight of the students column, by making it static.'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 1542ed961f6..8d86c04a74a 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -154,6 +154,7 @@ $string['answer'] = 'Answer'; $string['any'] = 'Any'; $string['approve'] = 'Approve'; $string['appearance'] = 'Appearance'; +$string['apply'] = 'Apply'; $string['applyfilters'] = 'Apply filters'; $string['areyousure'] = 'Are you sure?'; $string['areyousuretorestorethis'] = 'Do you want to continue?';