MDL-76134 gradebook: Hook into tertiary dropdown item

This commit is contained in:
Mathew May
2023-01-09 09:22:20 +08:00
parent 6b0d3b3eae
commit 712f219c7c
11 changed files with 320 additions and 17 deletions
+10
View File
@@ -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 <mathew.solutions>
* @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
File diff suppressed because one or more lines are too long
+169
View File
@@ -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 <http://www.gnu.org/licenses/>.
/**
* A small dropdown to filter users within the gradebook.
*
* @module core_grades/searchwidget/initials
* @copyright 2022 Mathew May <mathew.solutions>
* @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;
};
+48
View File
@@ -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.
*
@@ -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;
}
}
+7 -16
View File
@@ -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);
+9
View File
@@ -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: '';
}
@@ -69,5 +69,11 @@
</div>
<div class="navitem-divider"></div>
{{/generalnavselector}}
{{#initialselector}}
<div class="navitem d-flex flex-column align-self-center">
{{>core_grades/tertiary_navigation_dropdown}}
</div>
<div class="navitem-divider"></div>
{{/initialselector}}
</div>
</div>
@@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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"
}
}}
<form class="initialsdropdownform" action="#" data-courseid="{{courseid}}">
{{{initialsbars}}}
<div class="d-flex flex-row justify-content-end">
<input class="btn btn-outline-secondary pull-right mx-2" data-action="cancel" type="submit" value="{{#str}}closebuttontitle{{/str}}">
<input class="btn btn-primary pull-right" data-action="save" type="submit" value="{{#str}}apply{{/str}}">
</div>
</form>
+4
View File
@@ -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.';
+1
View File
@@ -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?';