mirror of
https://github.com/moodle/moodle.git
synced 2025-04-14 04:52:36 +02:00
MDL-75274 gradereport_grader: Column collapsing
This commit is contained in:
parent
e285841a9a
commit
1a1939ac29
106
grade/classes/external/get_gradeitems.php
vendored
Normal file
106
grade/classes/external/get_gradeitems.php
vendored
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
namespace core_grades\external;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
use context_course;
|
||||
use core_external\external_api;
|
||||
use core_external\external_function_parameters;
|
||||
use core_external\external_multiple_structure;
|
||||
use core_external\external_single_structure;
|
||||
use core_external\external_value;
|
||||
use core_external\external_warnings;
|
||||
use core_external\restricted_context_exception;
|
||||
use grade_item;
|
||||
|
||||
require_once($CFG->libdir . '/gradelib.php');
|
||||
|
||||
/**
|
||||
* External grade get gradeitems API implementation
|
||||
*
|
||||
* @package core_grades
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @category external
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class get_gradeitems 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)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a course ID find the grading objects and return their names & IDs.
|
||||
*
|
||||
* @param int $courseid
|
||||
* @return array
|
||||
* @throws restricted_context_exception
|
||||
* @throws \invalid_parameter_exception
|
||||
*/
|
||||
public static function execute(int $courseid): array {
|
||||
$params = self::validate_parameters(
|
||||
self::execute_parameters(),
|
||||
[
|
||||
'courseid' => $courseid
|
||||
]
|
||||
);
|
||||
|
||||
$warnings = [];
|
||||
$context = context_course::instance($params['courseid']);
|
||||
parent::validate_context($context);
|
||||
|
||||
$allgradeitems = grade_item::fetch_all(['courseid' => $params['courseid']]);
|
||||
$gradeitems = array_filter($allgradeitems, function($item) {
|
||||
$item->itemname = $item->get_name();
|
||||
$item->category = $item->get_parent_category()->get_name();
|
||||
return $item->gradetype != GRADE_TYPE_NONE && !$item->is_category_item() && !$item->is_course_item();
|
||||
});
|
||||
|
||||
return [
|
||||
'gradeItems' => $gradeitems,
|
||||
'warnings' => $warnings,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description of what gradeitems fetch should return.
|
||||
*
|
||||
* @return external_single_structure
|
||||
*/
|
||||
public static function execute_returns(): external_single_structure {
|
||||
return new external_single_structure([
|
||||
'gradeItems' => new external_multiple_structure(
|
||||
new external_single_structure([
|
||||
'id' => new external_value(PARAM_ALPHANUM, 'An ID for the grade item', VALUE_REQUIRED),
|
||||
'itemname' => new external_value(PARAM_TEXT, 'The full name of the grade item', VALUE_REQUIRED),
|
||||
'category' => new external_value(PARAM_TEXT, 'The grade category of the grade item', VALUE_OPTIONAL),
|
||||
])
|
||||
),
|
||||
'warnings' => new external_warnings(),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1539,7 +1539,7 @@ class grade_structure {
|
||||
* @param bool $withdescription Show description if defined by this item.
|
||||
* @param bool $fulltotal If the item is a category total, returns $categoryname."total"
|
||||
* instead of "Category total" or "Course total"
|
||||
* @param moodle_url|null $sortlink Link to sort column.
|
||||
* @param moodle_url|null $sortlink Link to sort column.
|
||||
*
|
||||
* @return string header
|
||||
*/
|
||||
@ -1563,19 +1563,25 @@ class grade_structure {
|
||||
|
||||
if ($sortlink) {
|
||||
$url = $sortlink;
|
||||
$header = html_writer::link($url, $header,
|
||||
['title' => $titleunescaped, 'class' => 'gradeitemheader']);
|
||||
}
|
||||
|
||||
if (!$sortlink) {
|
||||
$header = html_writer::link($url, $header, [
|
||||
'title' => $titleunescaped,
|
||||
'class' => 'gradeitemheader '
|
||||
]);
|
||||
} else {
|
||||
if ($withlink && $url = $this->get_activity_link($element)) {
|
||||
$a = new stdClass();
|
||||
$a->name = get_string('modulename', $element['object']->itemmodule);
|
||||
$a->title = $titleunescaped;
|
||||
$title = get_string('linktoactivity', 'grades', $a);
|
||||
$header = html_writer::link($url, $header, ['title' => $title, 'class' => 'gradeitemheader']);
|
||||
$header = html_writer::link($url, $header, [
|
||||
'title' => $title,
|
||||
'class' => 'gradeitemheader ',
|
||||
]);
|
||||
} else {
|
||||
$header = html_writer::span($header, 'gradeitemheader', ['title' => $titleunescaped, 'tabindex' => '0']);
|
||||
$header = html_writer::span($header, 'gradeitemheader ', [
|
||||
'title' => $titleunescaped,
|
||||
'tabindex' => '0'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2460,7 +2466,7 @@ class grade_structure {
|
||||
}
|
||||
}
|
||||
|
||||
$class = 'grade_icons';
|
||||
$class = 'grade_icons data-collapse_gradeicons';
|
||||
if (isset($element['type']) && ($element['type'] == 'category')) {
|
||||
$class = 'category_grade_icons';
|
||||
}
|
||||
@ -2526,14 +2532,18 @@ class grade_structure {
|
||||
$element, $gpr, $mode, $context, true);
|
||||
$context->advancedgradingurl = $this->get_advanced_grading_link($element, $gpr);
|
||||
}
|
||||
}
|
||||
|
||||
if ($element['type'] == 'item') {
|
||||
$context->divider1 = true;
|
||||
}
|
||||
|
||||
if (($element['type'] == 'item') ||
|
||||
(($element['type'] == 'userfield') && ($element['name'] !== 'fullname'))) {
|
||||
$context->divider2 = true;
|
||||
}
|
||||
|
||||
if (!empty($USER->editing) || $mode == 'setup') {
|
||||
if (($element['type'] !== 'userfield') && ($mode !== 'setup')) {
|
||||
if (($element['type'] == 'userfield') && ($element['name'] !== 'fullname')) {
|
||||
$context->divider2 = true;
|
||||
} else if (($mode !== 'setup') && ($element['type'] !== 'userfield')) {
|
||||
$context->divider1 = true;
|
||||
$context->divider2 = true;
|
||||
}
|
||||
@ -2577,6 +2587,10 @@ class grade_structure {
|
||||
$context->descendingurl = $this->get_sorting_link($sortlink, $gpr, 'desc');
|
||||
}
|
||||
}
|
||||
if ($mode !== 'setup') {
|
||||
$context = grade_report::get_additional_context($this->context, $this->courseid,
|
||||
$element, $gpr, $mode, $context);
|
||||
}
|
||||
} else if ($element['type'] == 'category') {
|
||||
$context->datatype = 'category';
|
||||
if ($mode !== 'setup') {
|
||||
|
3
grade/report/grader/amd/build/collapse.min.js
vendored
Normal file
3
grade/report/grader/amd/build/collapse.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
grade/report/grader/amd/build/collapse.min.js.map
Normal file
1
grade/report/grader/amd/build/collapse.min.js.map
Normal file
File diff suppressed because one or more lines are too long
10
grade/report/grader/amd/build/collapse/repository.min.js
vendored
Normal file
10
grade/report/grader/amd/build/collapse/repository.min.js
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
define("gradereport_grader/collapse/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
|
||||
/**
|
||||
* A repo for the collapsing in the grader report.
|
||||
*
|
||||
* @module gradereport_grader/collapse/repository
|
||||
* @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.gradeItems=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.gradeItems=courseid=>{const request={methodname:"core_grades_get_gradeitems",args:{courseid:courseid}};return _ajax.default.call([request])[0]}}));
|
||||
|
||||
//# sourceMappingURL=repository.min.js.map
|
@ -0,0 +1 @@
|
||||
{"version":3,"file":"repository.min.js","sources":["../../src/collapse/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A repo for the collapsing in the grader report.\n *\n * @module gradereport_grader/collapse/repository\n * @copyright 2022 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport ajax from 'core/ajax';\n\n/**\n * Fetch all the information on gradeitems we'll need in the column collapser.\n *\n * @method gradeItems\n * @param {Number} courseid What course to fetch the gradeitems for\n * @return {object} jQuery promise\n */\nexport const gradeItems = (courseid) => {\n const request = {\n methodname: 'core_grades_get_gradeitems',\n args: {\n courseid: courseid,\n },\n };\n return ajax.call([request])[0];\n};\n"],"names":["courseid","request","methodname","args","ajax","call"],"mappings":";;;;;;;gKAgC2BA,iBACjBC,QAAU,CACZC,WAAY,6BACZC,KAAM,CACFH,SAAUA,kBAGXI,cAAKC,KAAK,CAACJ,UAAU"}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
524
grade/report/grader/amd/src/collapse.js
Normal file
524
grade/report/grader/amd/src/collapse.js
Normal file
@ -0,0 +1,524 @@
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Allow the user to show and hide columns of the report at will.
|
||||
*
|
||||
* @module gradereport_grader/collapse
|
||||
* @copyright 2023 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
import * as Repository from 'gradereport_grader/collapse/repository';
|
||||
import GradebookSearchClass from 'gradereport_grader/search/search_class';
|
||||
import {renderForPromise, replaceNodeContents, replaceNode} from 'core/templates';
|
||||
import {debounce} from 'core/utils';
|
||||
import $ from 'jquery';
|
||||
import {get_strings as getStrings} from 'core/str';
|
||||
import CustomEvents from "core/custom_interaction_events";
|
||||
import storage from 'core/localstorage';
|
||||
import {addIconToContainer} from 'core/loadingicon';
|
||||
import Notification from 'core/notification';
|
||||
import Pending from 'core/pending';
|
||||
|
||||
// Contain our selectors within this file until they could be of use elsewhere.
|
||||
const selectors = {
|
||||
component: '.collapse-columns',
|
||||
formDropdown: '.columnsdropdownform',
|
||||
formItems: {
|
||||
cancel: 'cancel',
|
||||
save: 'save',
|
||||
checked: 'input[type="checkbox"]:checked'
|
||||
},
|
||||
hider: 'hide',
|
||||
expand: 'expand',
|
||||
colVal: '[data-col]',
|
||||
itemVal: '[data-itemid]',
|
||||
content: '[data-collapse="content"]',
|
||||
sort: '[data-collapse="sort"]',
|
||||
expandbutton: '[data-collapse="expandbutton"]',
|
||||
avgrowcell: '[data-collapse="avgrowcell"]',
|
||||
menu: '[data-collapse="menu"]',
|
||||
icons: '.data-collapse_gradeicons',
|
||||
count: '[data-collapse="count"]',
|
||||
placeholder: '.collapsecolumndropdown [data-region="placeholder"]',
|
||||
fullDropdown: '.collapsecolumndropdown',
|
||||
};
|
||||
|
||||
const countIndicator = document.querySelector(selectors.count);
|
||||
|
||||
export default class ColumnSearch extends GradebookSearchClass {
|
||||
|
||||
userID = -1;
|
||||
courseID = null;
|
||||
defaultSort = '';
|
||||
|
||||
nodes = [];
|
||||
|
||||
gradeStrings = null;
|
||||
userStrings = null;
|
||||
stringMap = [];
|
||||
|
||||
static init(userID, courseID, defaultSort) {
|
||||
return new ColumnSearch(userID, courseID, defaultSort);
|
||||
}
|
||||
|
||||
constructor(userID, courseID, defaultSort) {
|
||||
super();
|
||||
this.userID = userID;
|
||||
this.courseID = courseID;
|
||||
this.defaultSort = defaultSort;
|
||||
this.component = document.querySelector(selectors.component);
|
||||
|
||||
const pendingPromise = new Pending();
|
||||
// Display a loader whilst collapsing appropriate columns (based on the locally stored state for the current user).
|
||||
addIconToContainer(document.querySelector('.gradeparent')).then((loader) => {
|
||||
setTimeout(() => {
|
||||
// Get the users' checked columns to change.
|
||||
this.getDataset().forEach((item) => {
|
||||
this.nodesUpdate(item);
|
||||
});
|
||||
this.renderDefault();
|
||||
|
||||
// Once the grade categories have been re-collapsed, remove the loader and display the Gradebook setup content.
|
||||
loader.remove();
|
||||
document.querySelector('.gradereport-grader-table').classList.remove('d-none');
|
||||
}, 10);
|
||||
}).then(() => pendingPromise.resolve()).catch(Notification.exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* The overall div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
setComponentSelector() {
|
||||
return '.collapse-columns';
|
||||
}
|
||||
|
||||
/**
|
||||
* The dropdown div that contains the searching widget result space.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
setDropdownSelector() {
|
||||
return '.searchresultitemscontainer';
|
||||
}
|
||||
|
||||
/**
|
||||
* The triggering div that contains the searching widget.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
setTriggerSelector() {
|
||||
return '.collapsecolumn';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the dataset that we will be searching upon.
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
getDataset() {
|
||||
if (!this.dataset) {
|
||||
const cols = this.fetchDataset();
|
||||
this.dataset = JSON.parse(cols) ? JSON.parse(cols).split(',') : [];
|
||||
}
|
||||
this.datasetSize = this.dataset.length;
|
||||
return this.dataset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data we will be searching against in this component.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
fetchDataset() {
|
||||
return storage.get(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a user performs an action, update the users' preferences.
|
||||
*/
|
||||
setPreferences() {
|
||||
storage.set(`gradereport_grader_collapseditems_${this.courseID}_${this.userID}`,
|
||||
JSON.stringify(this.getDataset().join(','))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register clickable event listeners.
|
||||
*/
|
||||
registerClickHandlers() {
|
||||
// Register click events within the component.
|
||||
this.component.addEventListener('click', this.clickHandler.bind(this));
|
||||
|
||||
document.addEventListener('click', this.docClickHandler.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* The handler for when a user interacts with the component.
|
||||
*
|
||||
* @param {MouseEvent} e The triggering event that we are working with.
|
||||
*/
|
||||
clickHandler(e) {
|
||||
super.clickHandler(e);
|
||||
// Prevent BS from closing the dropdown if they click elsewhere within the dropdown besides the form.
|
||||
if (e.target.closest(selectors.fullDropdown)) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Externally defined click function to improve memory handling.
|
||||
*
|
||||
* @param {MouseEvent} e
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async docClickHandler(e) {
|
||||
if (e.target.dataset.hider === selectors.hider) {
|
||||
e.preventDefault();
|
||||
const desiredToHide = e.target.closest(selectors.colVal) ?
|
||||
e.target.closest(selectors.colVal)?.dataset.col :
|
||||
e.target.closest(selectors.itemVal)?.dataset.itemid;
|
||||
const idx = this.getDataset().indexOf(desiredToHide);
|
||||
if (idx === -1) {
|
||||
this.getDataset().push(desiredToHide);
|
||||
}
|
||||
await this.prefcountpippe();
|
||||
|
||||
this.nodesUpdate(desiredToHide);
|
||||
}
|
||||
|
||||
if (e.target.closest('button')?.dataset.hider === selectors.expand) {
|
||||
e.preventDefault();
|
||||
const desiredToHide = e.target.closest(selectors.colVal) ?
|
||||
e.target.closest(selectors.colVal)?.dataset.col :
|
||||
e.target.closest(selectors.itemVal)?.dataset.itemid;
|
||||
const idx = this.getDataset().indexOf(desiredToHide);
|
||||
this.getDataset().splice(idx, 1);
|
||||
|
||||
await this.prefcountpippe();
|
||||
|
||||
this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.col);
|
||||
this.nodesUpdate(e.target.closest(selectors.colVal)?.dataset.itemid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any keyboard inputs.
|
||||
*/
|
||||
registerInputEvents() {
|
||||
// Register & handle the text input.
|
||||
this.searchInput.addEventListener('input', debounce(async() => {
|
||||
this.setSearchTerms(this.searchInput.value);
|
||||
// We can also require a set amount of input before search.
|
||||
if (this.searchInput.value === '') {
|
||||
// Hide the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.add('d-none');
|
||||
} else {
|
||||
// Display the "clear" search button in the search bar.
|
||||
this.clearSearchButton.classList.remove('d-none');
|
||||
}
|
||||
// User has given something for us to filter against.
|
||||
await this.filterrenderpipe();
|
||||
}, 300));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the form submission within the dropdown.
|
||||
*/
|
||||
registerFormEvents() {
|
||||
const form = this.component.querySelector(selectors.formDropdown);
|
||||
const events = [
|
||||
'click',
|
||||
CustomEvents.events.activate,
|
||||
CustomEvents.events.keyboardActivate
|
||||
];
|
||||
CustomEvents.define(document, events);
|
||||
|
||||
// Register clicks & keyboard form handling.
|
||||
events.forEach((event) => {
|
||||
form.addEventListener(event, (e) => {
|
||||
// Stop Bootstrap from being clever.
|
||||
e.stopPropagation();
|
||||
const submitBtn = form.querySelector(`[data-action="${selectors.formItems.save}"`);
|
||||
if (e.target.closest('input')) {
|
||||
const checkedCount = Array.from(form.querySelectorAll(selectors.formItems.checked)).length;
|
||||
// Check if any are clicked or not then change disabled.
|
||||
submitBtn.disabled = checkedCount <= 0;
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Stop Bootstrap from being clever.
|
||||
this.searchInput.addEventListener(event, e => e.stopPropagation());
|
||||
this.clearSearchButton.addEventListener(event, async(e) => {
|
||||
e.stopPropagation();
|
||||
this.searchInput.value = '';
|
||||
this.setSearchTerms(this.searchInput.value);
|
||||
await this.filterrenderpipe();
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async(e) => {
|
||||
e.preventDefault();
|
||||
if (e.submitter.dataset.action === selectors.formItems.cancel) {
|
||||
$(this.component).dropdown('toggle');
|
||||
return;
|
||||
}
|
||||
// Get the users' checked columns to change.
|
||||
const checkedItems = [...form.elements].filter(item => item.checked);
|
||||
checkedItems.forEach((item) => {
|
||||
const idx = this.getDataset().indexOf(item.dataset.collapse);
|
||||
this.getDataset().splice(idx, 1);
|
||||
this.nodesUpdate(item.dataset.collapse);
|
||||
});
|
||||
await this.prefcountpippe();
|
||||
});
|
||||
}
|
||||
|
||||
nodesUpdate(item) {
|
||||
const colNodesToHide = [...document.querySelectorAll(`[data-col="${item}"]`)];
|
||||
const itemIDNodesToHide = [...document.querySelectorAll(`[data-itemid="${item}"]`)];
|
||||
this.nodes = [...colNodesToHide, ...itemIDNodesToHide];
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user preferences, count display then render the results.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async prefcountpippe() {
|
||||
this.setPreferences();
|
||||
this.countUpdate();
|
||||
await this.filterrenderpipe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dictate to the search component how and what we want to match upon.
|
||||
*
|
||||
* @param {Array} filterableData
|
||||
* @returns {Array} An array of objects containing the system reference and the user readable value.
|
||||
*/
|
||||
async filterDataset(filterableData) {
|
||||
const stringUserMap = await this.fetchRequiredUserStrings();
|
||||
const stringGradeMap = await this.fetchRequiredGradeStrings();
|
||||
// Custom user profile fields are not in our string map and need a bit of extra love.
|
||||
const customFieldMap = this.fetchCustomFieldValues();
|
||||
this.stringMap = new Map([...stringGradeMap, ...stringUserMap, ...customFieldMap]);
|
||||
|
||||
const searching = filterableData.map(s => {
|
||||
const mapObj = this.stringMap.get(s);
|
||||
if (mapObj === undefined) {
|
||||
return {key: s, string: s};
|
||||
}
|
||||
return {
|
||||
key: s,
|
||||
string: mapObj.itemname ?? this.stringMap.get(s),
|
||||
category: mapObj.category ?? '',
|
||||
};
|
||||
});
|
||||
// Sometimes we just want to show everything.
|
||||
if (this.getPreppedSearchTerm() === '') {
|
||||
return searching;
|
||||
}
|
||||
// Other times we want to actually filter the content.
|
||||
return searching.filter((col) => {
|
||||
return col.string.toString().toLowerCase().includes(this.getPreppedSearchTerm());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given we have a subset of the dataset, set the field that we matched upon to inform the end user.
|
||||
*/
|
||||
filterMatchDataset() {
|
||||
this.setMatchedResults(
|
||||
this.getMatchedResults().map((column) => {
|
||||
return {
|
||||
name: column.key,
|
||||
displayName: column.string ?? column.key,
|
||||
category: column.category ?? '',
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update any changeable nodes, filter and then render the result.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async filterrenderpipe() {
|
||||
this.updateNodes();
|
||||
this.setMatchedResults(await this.filterDataset(this.getDataset()));
|
||||
this.filterMatchDataset();
|
||||
await this.renderDropdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* With an array of nodes, switch their classes and values.
|
||||
*/
|
||||
updateDisplay() {
|
||||
this.nodes.forEach((element) => {
|
||||
const content = element.querySelector(selectors.content);
|
||||
const sort = element.querySelector(selectors.sort);
|
||||
const expandButton = element.querySelector(selectors.expandbutton);
|
||||
const avgRowCell = element.querySelector(selectors.avgrowcell);
|
||||
const nodeSet = [
|
||||
element.querySelector(selectors.menu),
|
||||
element.querySelector(selectors.icons),
|
||||
content
|
||||
];
|
||||
|
||||
// This can be further improved to reduce redundant similar calls.
|
||||
if (element.classList.contains('cell')) {
|
||||
// The column is actively being sorted, lets reset that and reload the page.
|
||||
if (sort !== null) {
|
||||
window.location = this.defaultSort;
|
||||
}
|
||||
if (content === null) {
|
||||
if (avgRowCell.classList.contains('d-none')) {
|
||||
avgRowCell?.classList.remove('d-none');
|
||||
avgRowCell?.setAttribute('aria-hidden', 'false');
|
||||
} else {
|
||||
avgRowCell?.classList.add('d-none');
|
||||
avgRowCell?.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
} else if (content.classList.contains('d-none')) {
|
||||
// We should always have content but some cells do not contain menus or other actions.
|
||||
element.classList.remove('collapsed');
|
||||
content.classList.add('d-flex');
|
||||
nodeSet.forEach(node => {
|
||||
node?.classList.remove('d-none');
|
||||
node?.setAttribute('aria-hidden', 'false');
|
||||
});
|
||||
expandButton?.classList.add('d-none');
|
||||
expandButton?.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
element.classList.add('collapsed');
|
||||
content.classList.remove('d-flex');
|
||||
nodeSet.forEach(node => {
|
||||
node?.classList.add('d-none');
|
||||
node?.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
expandButton?.classList.remove('d-none');
|
||||
expandButton?.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the visual count of collapsed columns or hide the count all together.
|
||||
*/
|
||||
countUpdate() {
|
||||
countIndicator.textContent = this.getDatasetSize();
|
||||
if (this.getDatasetSize() > 0) {
|
||||
this.component.parentElement.classList.add('d-flex');
|
||||
this.component.parentElement.classList.remove('d-none');
|
||||
} else {
|
||||
this.component.parentElement.classList.remove('d-flex');
|
||||
this.component.parentElement.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node by default we want our form to exist.
|
||||
*/
|
||||
async renderDefault() {
|
||||
this.setMatchedResults(await this.filterDataset(this.getDataset()));
|
||||
this.filterMatchDataset();
|
||||
|
||||
// Update the collapsed button pill.
|
||||
this.countUpdate();
|
||||
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapsebody', {
|
||||
'results': this.getMatchedResults(),
|
||||
'userid': this.userID,
|
||||
});
|
||||
replaceNode(selectors.placeholder, html, js);
|
||||
this.updateNodes();
|
||||
|
||||
// Given we now have the body, we can set up more triggers.
|
||||
this.registerFormEvents();
|
||||
this.registerInputEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the content then replace the node.
|
||||
*/
|
||||
async renderDropdown() {
|
||||
const {html, js} = await renderForPromise('gradereport_grader/collapse/collapseresults', {
|
||||
'results': this.getMatchedResults(),
|
||||
'searchTerm': this.getSearchTerm(),
|
||||
});
|
||||
replaceNodeContents(this.getHTMLElements().searchDropdown, html, js);
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have any custom user profile fields, grab their system & readable names to add to our string map.
|
||||
*
|
||||
* @returns {[string,*][]} An array of associated string arrays ready for our map.
|
||||
*/
|
||||
fetchCustomFieldValues() {
|
||||
const customFields = document.querySelectorAll('[data-collapse-name]');
|
||||
// Cast from NodeList to array to grab all the values.
|
||||
return [...customFields].map(field => [field.parentElement.dataset.col, field.dataset.collapseName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the set of profile fields we can possibly search, fetch their strings,
|
||||
* so we can report to screen readers the field that matched.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
fetchRequiredUserStrings() {
|
||||
if (!this.userStrings) {
|
||||
const requiredStrings = [
|
||||
'username',
|
||||
'firstname',
|
||||
'lastname',
|
||||
'email',
|
||||
'city',
|
||||
'country',
|
||||
'department',
|
||||
'institution',
|
||||
'idnumber',
|
||||
'phone1',
|
||||
'phone2',
|
||||
];
|
||||
this.userStrings = getStrings(requiredStrings.map((key) => ({key})))
|
||||
.then((stringArray) => new Map(
|
||||
requiredStrings.map((key, index) => ([key, stringArray[index]]))
|
||||
));
|
||||
}
|
||||
return this.userStrings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the set of gradable items we can possibly search, fetch their strings,
|
||||
* so we can report to screen readers the field that matched.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
fetchRequiredGradeStrings() {
|
||||
if (!this.gradeStrings) {
|
||||
this.gradeStrings = Repository.gradeItems(this.courseID)
|
||||
.then((result) => new Map(
|
||||
result.gradeItems.map(key => ([key.id, key]))
|
||||
));
|
||||
}
|
||||
return this.gradeStrings;
|
||||
}
|
||||
}
|
41
grade/report/grader/amd/src/collapse/repository.js
Normal file
41
grade/report/grader/amd/src/collapse/repository.js
Normal file
@ -0,0 +1,41 @@
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* A repo for the collapsing in the grader report.
|
||||
*
|
||||
* @module gradereport_grader/collapse/repository
|
||||
* @copyright 2022 Mathew May <mathew.solutions>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
import ajax from 'core/ajax';
|
||||
|
||||
/**
|
||||
* Fetch all the information on gradeitems we'll need in the column collapser.
|
||||
*
|
||||
* @method gradeItems
|
||||
* @param {Number} courseid What course to fetch the gradeitems for
|
||||
* @return {object} jQuery promise
|
||||
*/
|
||||
export const gradeItems = (courseid) => {
|
||||
const request = {
|
||||
methodname: 'core_grades_get_gradeitems',
|
||||
args: {
|
||||
courseid: courseid,
|
||||
},
|
||||
};
|
||||
return ajax.call([request])[0];
|
||||
};
|
@ -76,11 +76,15 @@ export default class {
|
||||
$component = $(this.component);
|
||||
|
||||
constructor() {
|
||||
this.setSearchTerms(this.searchInput.value ?? '');
|
||||
// If we have a search input, try to get the value otherwise fallback.
|
||||
this.setSearchTerms(this.searchInput?.value ?? '');
|
||||
// Begin handling the base search component.
|
||||
this.registerClickHandlers();
|
||||
this.registerKeyHandlers();
|
||||
this.registerInputHandlers();
|
||||
// Conditionally set up the input handler since we don't know exactly how we were called.
|
||||
if (this.searchInput !== null) {
|
||||
this.registerInputHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -252,6 +256,9 @@ export default class {
|
||||
this.resultNodes = [...this.component.querySelectorAll(this.selectors.resultitems)];
|
||||
this.currentNode = this.resultNodes.find(r => r.id === document.activeElement.id);
|
||||
this.currentViewAll = this.component.querySelector(this.selectors.viewall);
|
||||
this.clearSearchButton = this.component.querySelector(this.selectors.clearSearch);
|
||||
this.searchInput = this.component.querySelector(this.selectors.input);
|
||||
this.searchDropdown = this.component.querySelector(this.selectors.dropdown);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -107,6 +107,21 @@ class action_bar extends \core_grades\output\action_bar {
|
||||
false,
|
||||
);
|
||||
$data['searchdropdown'] = $searchdropdown->export_for_template($output);
|
||||
|
||||
$collapse = new gradebook_dropdown(
|
||||
true,
|
||||
get_string('collapsedcolumns', 'gradereport_grader', 0),
|
||||
null,
|
||||
'collapse-columns',
|
||||
'collapsecolumn',
|
||||
'collapsecolumndropdown p-3 flex-column',
|
||||
null,
|
||||
true,
|
||||
);
|
||||
$data['collapsedcolumns'] = [
|
||||
'classes' => 'd-none',
|
||||
'content' => $collapse->export_for_template($output)
|
||||
];
|
||||
}
|
||||
|
||||
return $data;
|
||||
|
@ -127,6 +127,14 @@ if ($sort) {
|
||||
$sort = strtoupper($sort);
|
||||
}
|
||||
$report = new grade_report_grader($courseid, $gpr, $context, $page, $sortitemid, $sort);
|
||||
|
||||
// We call this a little later since we need some info from the grader report.
|
||||
$PAGE->requires->js_call_amd('gradereport_grader/collapse', 'init', [
|
||||
'userID' => $USER->id,
|
||||
'courseID' => $courseid,
|
||||
'defaultSort' => $report->get_default_sortable()
|
||||
]);
|
||||
|
||||
$numusers = $report->get_numusers(true, true);
|
||||
|
||||
$actionbar = new \gradereport_grader\output\action_bar($context, $report, $numusers);
|
||||
|
@ -23,6 +23,7 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['collapsedcolumns'] = 'Collapsed columns <span class="badge badge-pill badge-primary ml-1" data-collapse="count">{$a}</span>';
|
||||
$string['eventgradereportviewed'] = 'Grader report viewed';
|
||||
$string['grader:manage'] = 'Manage the grader report';
|
||||
$string['grader:view'] = 'View grader report';
|
||||
@ -43,6 +44,7 @@ $string['privacy:metadata:preference:grade_report_showranges'] = 'Whether to dis
|
||||
$string['privacy:metadata:preference:grade_report_showuserimage'] = 'Whether to show the user\'s profile image next to the name';
|
||||
$string['privacy:metadata:preference:grade_report_studentsperpage'] = 'The number of students displayed per page in the grader report';
|
||||
$string['privacy:request:preference:grade_report_grader_collapsed_categories'] = 'You have some gradebook categories collapsed in the "{$a->name}" course';
|
||||
$string['reopencolumn'] = 'Reopen {$a} column';
|
||||
$string['summarygrader'] = 'A table with the names of students in the first column, with assessable activities grouped by course and category across the top.';
|
||||
$string['showingxofy'] = 'Showing {$a->found} of {$a->total}';
|
||||
$string['useractivitygrade'] = '{$a} grade';
|
||||
|
@ -666,11 +666,19 @@ class grade_report_grader extends grade_report {
|
||||
foreach ($extrafields as $field) {
|
||||
$fieldheader = new html_table_cell();
|
||||
$fieldheader->attributes['class'] = 'userfield user' . $field;
|
||||
$fieldheader->attributes['data-col'] = $field;
|
||||
$fieldheader->scope = 'col';
|
||||
$fieldheader->header = true;
|
||||
|
||||
$collapsecontext = ['field' => $field, 'name' => $field];
|
||||
|
||||
$collapsedicon = $OUTPUT->render_from_template('gradereport_grader/collapse/icon', $collapsecontext);
|
||||
// Need to wrap the button into a div with our hooking element for user items, gradeitems already have this.
|
||||
$collapsedicon = html_writer::div($collapsedicon, 'd-none', ['data-collapse' => 'expandbutton']);
|
||||
|
||||
$element = ['type' => 'userfield', 'name' => $field];
|
||||
$fieldheader->text = $arrows[$field] .
|
||||
$this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr, $this->baseurl);
|
||||
$this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr, $this->baseurl) . $collapsedicon;
|
||||
$headerrow->cells[] = $fieldheader;
|
||||
}
|
||||
|
||||
@ -723,8 +731,12 @@ class grade_report_grader extends grade_report {
|
||||
foreach ($extrafields as $field) {
|
||||
$fieldcell = new html_table_cell();
|
||||
$fieldcell->attributes['class'] = 'userfield user' . $field;
|
||||
$fieldcell->attributes['data-col'] = $field;
|
||||
$fieldcell->header = false;
|
||||
$fieldcell->text = s($user->{$field});
|
||||
$fieldcell->text = html_writer::tag('div', s($user->{$field}), [
|
||||
'data-collapse' => 'content'
|
||||
]);
|
||||
|
||||
$userrow->cells[] = $fieldcell;
|
||||
}
|
||||
|
||||
@ -836,6 +848,15 @@ class grade_report_grader extends grade_report {
|
||||
}
|
||||
}
|
||||
|
||||
$collapsecontext = [
|
||||
'field' => $element['object']->id,
|
||||
'name' => $element['object']->get_name(),
|
||||
];
|
||||
$collapsedicon = '';
|
||||
// We do not want grade category total items to be hidden away as it is controlled by something else.
|
||||
if (!$element['object']->is_aggregate_item()) {
|
||||
$collapsedicon = $OUTPUT->render_from_template('gradereport_grader/collapse/icon', $collapsecontext);
|
||||
}
|
||||
$headerlink = $this->gtree->get_element_header($element, true,
|
||||
true, false, false, true, $sortlink);
|
||||
|
||||
@ -871,6 +892,7 @@ class grade_report_grader extends grade_report {
|
||||
$context->arrow = $arrow;
|
||||
$context->singleview = $singleview;
|
||||
$context->statusicons = $statusicons;
|
||||
$context->collapsedicon = $collapsedicon;
|
||||
|
||||
$itemcell->text = $OUTPUT->render_from_template('gradereport_grader/headercell', $context);
|
||||
|
||||
@ -1166,7 +1188,7 @@ class grade_report_grader extends grade_report {
|
||||
$html = '';
|
||||
|
||||
$fulltable = new html_table();
|
||||
$fulltable->attributes['class'] = 'gradereport-grader-table';
|
||||
$fulltable->attributes['class'] = 'gradereport-grader-table d-none';
|
||||
$fulltable->id = 'user-grades';
|
||||
$fulltable->caption = get_string('summarygrader', 'gradereport_grader');
|
||||
$fulltable->captionhide = true;
|
||||
@ -1370,7 +1392,6 @@ class grade_report_grader extends grade_report {
|
||||
*/
|
||||
public function get_right_avg_row($rows=array(), $grouponly=false) {
|
||||
global $USER, $DB, $OUTPUT, $CFG;
|
||||
|
||||
if (!$this->canviewhidden) {
|
||||
// Totals might be affected by hiding, if user can not see hidden grades the aggregations might be altered
|
||||
// better not show them at all if user can not see all hidden grades.
|
||||
@ -1518,9 +1539,9 @@ class grade_report_grader extends grade_report {
|
||||
if (!isset($sumarray[$item->id]) || $meancount == 0) {
|
||||
$avgcell = new html_table_cell();
|
||||
$avgcell->attributes['class'] = $gradetypeclass . ' i'. $itemid;
|
||||
$avgcell->text = '-';
|
||||
$avgcell->attributes['data-itemid'] = $itemid;
|
||||
$avgcell->text = html_writer::div('-', '', ['data-collapse' => 'avgrowcell']);
|
||||
$avgrow->cells[] = $avgcell;
|
||||
|
||||
} else {
|
||||
$sum = $sumarray[$item->id];
|
||||
$avgradeval = $sum/$meancount;
|
||||
@ -1533,7 +1554,8 @@ class grade_report_grader extends grade_report {
|
||||
|
||||
$avgcell = new html_table_cell();
|
||||
$avgcell->attributes['class'] = $gradetypeclass . ' i'. $itemid;
|
||||
$avgcell->text = $gradehtml.$numberofgrades;
|
||||
$avgcell->attributes['data-itemid'] = $itemid;
|
||||
$avgcell->text = html_writer::div($gradehtml.$numberofgrades, '', ['data-collapse' => 'avgrowcell']);
|
||||
$avgrow->cells[] = $avgcell;
|
||||
}
|
||||
}
|
||||
@ -1839,7 +1861,7 @@ class grade_report_grader extends grade_report {
|
||||
* user idnumber
|
||||
* @return array An associative array of HTML sorting links+arrows
|
||||
*/
|
||||
public function get_sort_arrows(array $extrafields = array()) {
|
||||
public function get_sort_arrows(array $extrafields = []) {
|
||||
global $CFG;
|
||||
$arrows = array();
|
||||
$sortlink = clone($this->baseurl);
|
||||
@ -1879,8 +1901,15 @@ class grade_report_grader extends grade_report {
|
||||
}
|
||||
|
||||
foreach ($extrafields as $field) {
|
||||
$fieldlink = html_writer::link(new moodle_url($this->baseurl,
|
||||
array('sortitemid' => $field)), \core_user\fields::get_display_name($field));
|
||||
$attributes = [
|
||||
'data-collapse' => 'content'
|
||||
];
|
||||
// With additional user profile fields, we can't grab the name via WS, so conditionally add it to rip out of the DOM.
|
||||
if (preg_match(\core_user\fields::PROFILE_FIELD_REGEX, $field)) {
|
||||
$attributes['data-collapse-name'] = \core_user\fields::get_display_name($field);
|
||||
}
|
||||
$fieldlink = html_writer::link(new moodle_url($this->baseurl, ['sortitemid' => $field]),
|
||||
\core_user\fields::get_display_name($field), $attributes);
|
||||
$arrows[$field] = $fieldlink;
|
||||
|
||||
if ($field == $this->sortitemid) {
|
||||
@ -1925,6 +1954,38 @@ class grade_report_grader extends grade_report {
|
||||
return html_writer::link($urlnew, $title,
|
||||
['class' => 'dropdown-item', 'aria-label' => $title, 'aria-current' => $active, 'role' => 'menuitem']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the link to allow the field to collapse from the users view.
|
||||
*
|
||||
* @return string Dropdown menu link that'll trigger the collapsing functionality.
|
||||
* @throws coding_exception
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public function get_hide_show_link(): string {
|
||||
$link = new moodle_url('#', []);
|
||||
return html_writer::link(
|
||||
$link->out(false),
|
||||
get_string('collapse'),
|
||||
['class' => 'dropdown-item', 'data-hider' => 'hide', 'aria-label' => get_string('collapse'), 'role' => 'menuitem'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the base report link with some default sorting applied.
|
||||
*
|
||||
* @return string
|
||||
* @throws moodle_exception
|
||||
*/
|
||||
public function get_default_sortable(): string {
|
||||
$sortlink = new moodle_url('/grade/report/grader/index.php', [
|
||||
'id' => $this->courseid,
|
||||
'sortitemid' => 'firstname',
|
||||
'sort' => 'asc'
|
||||
]);
|
||||
$this->gpr->add_url_params($sortlink);
|
||||
return $sortlink->out(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1941,12 +2002,12 @@ class grade_report_grader extends grade_report {
|
||||
function gradereport_grader_get_report_link(context_course $context, int $courseid,
|
||||
array $element, grade_plugin_return $gpr, string $mode, ?stdClass $templatecontext): ?stdClass {
|
||||
|
||||
if ($mode == 'category') {
|
||||
static $report = null;
|
||||
if (!$report) {
|
||||
$report = new grade_report_grader($courseid, $gpr, $context);
|
||||
}
|
||||
static $report = null;
|
||||
if (!$report) {
|
||||
$report = new grade_report_grader($courseid, $gpr, $context);
|
||||
}
|
||||
|
||||
if ($mode == 'category') {
|
||||
if (!isset($templatecontext)) {
|
||||
$templatecontext = new stdClass();
|
||||
}
|
||||
@ -1977,6 +2038,17 @@ function gradereport_grader_get_report_link(context_course $context, int $course
|
||||
$templatecontext->fullmodeurl =
|
||||
$report->get_category_view_mode_link($url, $strswitchwhole, 'switch_whole', $fullmode);
|
||||
return $templatecontext;
|
||||
} else if ($mode == 'gradeitem') {
|
||||
if (($element['type'] == 'userfield') && ($element['name'] !== 'fullname')) {
|
||||
$templatecontext->columncollapse = $report->get_hide_show_link();
|
||||
$templatecontext->dataid = $element['name'];
|
||||
}
|
||||
|
||||
// We do not want grade category total items to be hidden away as it is controlled by something else.
|
||||
if (isset($element['object']->id) && !$element['object']->is_aggregate_item()) {
|
||||
$templatecontext->columncollapse = $report->get_hide_show_link();
|
||||
}
|
||||
return $templatecontext;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -110,6 +110,10 @@
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.path-grade-report-grader .gradeparent .highlightable.cell.collapsed {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.path-grade-report-grader .gradeparent .user.cell .userpicture {
|
||||
border: none;
|
||||
vertical-align: middle;
|
||||
@ -249,3 +253,8 @@
|
||||
border-top: 1px solid #dee2e6;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.collapsecolumndropdown.show {
|
||||
width: 275px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
@ -60,7 +60,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"groupselector": "<div class='group-selector'></div>"
|
||||
"groupselector": "<div class='group-selector'></div>",
|
||||
"collapsedcolumns": "<div class='collapse-columns'></div>"
|
||||
}
|
||||
}}
|
||||
<div class="container-fluid tertiary-navigation full-width-bottom-border">
|
||||
@ -89,5 +90,12 @@
|
||||
</div>
|
||||
<div class="navitem-divider"></div>
|
||||
{{/initialselector}}
|
||||
{{#collapsedcolumns}}
|
||||
<div class="navitem flex-column align-self-center ml-auto {{#classes}}{{.}}{{/classes}}" aria-live="polite">
|
||||
{{#content}}
|
||||
{{>core_grades/tertiary_navigation_dropdown}}
|
||||
{{/content}}
|
||||
</div>
|
||||
{{/collapsedcolumns}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,7 +33,7 @@
|
||||
"name": "grade[313][624]"
|
||||
}
|
||||
}}
|
||||
<div class="d-flex flex-column h-100">
|
||||
<div class="d-flex flex-column h-100" data-collapse="content">
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-grow-1">
|
||||
{{#iseditable}}
|
||||
|
53
grade/report/grader/templates/collapse/collapsebody.mustache
Normal file
53
grade/report/grader/templates/collapse/collapsebody.mustache
Normal file
@ -0,0 +1,53 @@
|
||||
{{!
|
||||
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 gradereport_grader/collapse/collapsebody
|
||||
|
||||
The body of the column collapsing dropdown that contains the form and subsequent results from the search.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "42",
|
||||
"displayName": "The meaning of life",
|
||||
"category": "Hitchhikers grade category"
|
||||
}
|
||||
],
|
||||
"searchTerm": "Meaning of",
|
||||
"userid": "42"
|
||||
}
|
||||
}}
|
||||
<div class="flex-column h-100 w-100">
|
||||
<span class="d-none" data-region="userid" data-userid="{{userid}}" aria-hidden="true"></span>
|
||||
|
||||
{{< core/search_input_auto }}
|
||||
{{$label}}{{#str}}
|
||||
searchcollapsedcolumns, core_grades
|
||||
{{/str}}{{/label}}
|
||||
{{$placeholder}}{{#str}}
|
||||
searchcollapsedcolumns, core_grades
|
||||
{{/str}}{{/placeholder}}
|
||||
{{/ core/search_input_auto }}
|
||||
|
||||
<form class="columnsdropdownform flex-column h-100">
|
||||
<ul class="searchresultitemscontainer overflow-auto py-2 px-1 list-group mx-0 text-truncate" role="menu" data-region="search-result-items-container" tabindex="-1">
|
||||
{{>gradereport_grader/collapse/collapseresults}}
|
||||
</ul>
|
||||
<div class="d-flex flex-row justify-content-end mt-2">
|
||||
<input class="btn btn-outline-secondary pull-right mx-2" data-action="cancel" type="submit" value="{{#str}}closebuttontitle{{/str}}">
|
||||
<input disabled class="btn btn-primary pull-right" data-action="save" type="submit" value="{{#str}}expand{{/str}}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,41 @@
|
||||
{{!
|
||||
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 gradereport_grader/collapse/collapseresultitems
|
||||
|
||||
Context variables required for this template:
|
||||
* name - The 'system' name that we search against.
|
||||
* displayName - The 'end user' name that the user can search against.
|
||||
* category - The category that a gradable item is within.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"name": "42",
|
||||
"displayName": "The meaning of life",
|
||||
"category": "Hitchhikers grade category"
|
||||
}
|
||||
}}
|
||||
<li class="w-100 result-row form-check mb-1" role="none">
|
||||
<input class="form-check-input" data-collapse="{{name}}" type="checkbox" value="" id="check-{{name}}">
|
||||
<label class="selected-option-info d-block pr-3 text-truncate form-check-label" for="check-{{name}}" role="menuitem" tabindex="-1" aria-label="{{#str}}viewresultsuser, gradereport_grader, {{displayName}}{{/str}}">
|
||||
<span class="selected-option-text w-100 p-0">
|
||||
{{displayName}}
|
||||
</span>
|
||||
{{#category}}
|
||||
<span class="d-block w-100 pull-left text-muted text-truncate small">
|
||||
{{category}}
|
||||
</span>
|
||||
{{/category}}
|
||||
</label>
|
||||
</li>
|
@ -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 gradereport_grader/collapse/collapseresults
|
||||
|
||||
Context variables required for this template:
|
||||
* results - An array of found columns that are currently hidden.
|
||||
* searchTerm - What the user is currently searching for.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"name": "42",
|
||||
"displayName": "The meaning of life",
|
||||
"category": "Hitchhikers grade category"
|
||||
}
|
||||
],
|
||||
"searchTerm": "Meaning of"
|
||||
}
|
||||
}}
|
||||
{{#results}}
|
||||
{{>gradereport_grader/collapse/collapseresultitems}}
|
||||
{{/results}}
|
||||
{{^results}}
|
||||
<span class="d-block my-4">{{#str}} noresultsfor, core_grades, {{searchTerm}}{{/str}}</span>
|
||||
{{/results}}
|
29
grade/report/grader/templates/collapse/icon.mustache
Normal file
29
grade/report/grader/templates/collapse/icon.mustache
Normal file
@ -0,0 +1,29 @@
|
||||
{{!
|
||||
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 gradereport_grader/collapse/icon
|
||||
|
||||
Context variables required for this template:
|
||||
* field - Either the shortname of the user field or a grade item ID.
|
||||
|
||||
Example context (json):
|
||||
{
|
||||
"field": "42",
|
||||
"name": "Meaning of life"
|
||||
}
|
||||
}}
|
||||
<button class="btn btn-link btn-icon icon-size-3" data-hider="expand" data-col="{{field}}">
|
||||
<i class="icon fa fa-plus m-0" title="Reopen column" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{#str}}reopencolumn, gradereport_grader, {{name}}{{/str}}</span>
|
||||
</button>
|
@ -28,7 +28,10 @@
|
||||
}
|
||||
}}
|
||||
<div class="d-flex flex-column h-100">
|
||||
<div class="d-flex">
|
||||
<div class="d-none flex-grow-1 align-items-start" data-collapse="expandbutton">
|
||||
{{{collapsedicon}}}
|
||||
</div>
|
||||
<div class="d-flex" data-collapse="content">
|
||||
<div class="d-flex flex-grow-1">
|
||||
{{{headerlink}}}
|
||||
{{{arrow}}}
|
||||
|
@ -45,10 +45,10 @@
|
||||
{{/profileimageurl}}
|
||||
</div>
|
||||
<div class="d-block pr-3 w-75">
|
||||
<span class="w-100 p-0 font-weight-bold">
|
||||
<span class="d-flex w-100 p-0 font-weight-bold">
|
||||
{{fullname}}
|
||||
</span>
|
||||
<span class="w-100 pull-left text-truncate small" aria-hidden="true">
|
||||
<span class="d-flex w-100 pull-left text-truncate small" aria-hidden="true">
|
||||
{{{matchingField}}}
|
||||
</span>
|
||||
<span class="sr-only" aria-label="{{#str}}usermatchedon, core_grades{{/str}}">{{matchingFieldName}}</span>
|
||||
|
@ -127,8 +127,21 @@ class behat_gradereport_grader extends behat_base {
|
||||
*/
|
||||
public function i_click_on_user_profile_field_menu(string $field) {
|
||||
|
||||
$xpath = "//table[@id='user-grades']//*[@data-type='" . $field . "']";
|
||||
$xpath = "//table[@id='user-grades']//*[@data-type='" . mb_strtolower($field) . "']";
|
||||
$this->execute("behat_general::i_click_on", array($this->escape($xpath), "xpath_element"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of partial named selectors.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_partial_named_selectors(): array {
|
||||
return [
|
||||
new behat_component_named_selector(
|
||||
'collapse search',
|
||||
[".//*[contains(concat(' ', @class, ' '), ' collapsecolumndropdown ')]"]
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
217
grade/report/grader/tests/behat/column_collapsing.feature
Normal file
217
grade/report/grader/tests/behat/column_collapsing.feature
Normal file
@ -0,0 +1,217 @@
|
||||
@core @javascript @gradereport @gradereport_grader
|
||||
Feature: Within the grader report, test that we can collapse columns
|
||||
In order to reduce usage of visual real estate
|
||||
As a teacher
|
||||
I need to be able to change how the report is displayed
|
||||
|
||||
Background:
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | groupmode |
|
||||
| Course 1 | C1 | 0 | 1 |
|
||||
And the following "grade categories" exist:
|
||||
| fullname | course |
|
||||
| Some cool grade category | C1 |
|
||||
And the following "custom profile fields" exist:
|
||||
| datatype | shortname | name |
|
||||
| text | enduro | Favourite enduro race |
|
||||
And the following "users" exist:
|
||||
| username | firstname | lastname | email | idnumber | phone1 | phone2 | department | institution | city | country |
|
||||
| teacher1 | Teacher | 1 | teacher1@example.com | t1 | 1234567892 | 1234567893 | ABC1 | ABCD | Perth | AU |
|
||||
| student1 | Student | 1 | student1@example.com | s1 | 3213078612 | 8974325612 | ABC1 | ABCD | Hanoi | VN |
|
||||
| student2 | Dummy | User | student2@example.com | s2 | 4365899871 | 7654789012 | ABC2 | ABCD | Tokyo | JP |
|
||||
| student3 | User | Example | student3@example.com | s3 | 3243249087 | 0875421745 | ABC2 | ABCD | Olney | GB |
|
||||
| student4 | User | Test | student4@example.com | s4 | 0987532523 | 2149871323 | ABC3 | ABCD | Tokyo | JP |
|
||||
| student5 | Turtle | Manatee | student5@example.com | s5 | 1239087780 | 9873623589 | ABC3 | ABCD | Perth | AU |
|
||||
And the following "course enrolments" exist:
|
||||
| user | course | role |
|
||||
| teacher1 | C1 | editingteacher |
|
||||
| student1 | C1 | student |
|
||||
| student2 | C1 | student |
|
||||
| student3 | C1 | student |
|
||||
| student4 | C1 | student |
|
||||
| student5 | C1 | student |
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name | gradecategory |
|
||||
| assign | C1 | a1 | Test assignment one | Some cool grade category |
|
||||
| assign | C1 | a3 | Test assignment three | Some cool grade category |
|
||||
And the following "activities" exist:
|
||||
| activity | course | idnumber | name |
|
||||
| assign | C1 | a2 | Test assignment two |
|
||||
| assign | C1 | a4 | Test assignment four |
|
||||
And the following config values are set as admin:
|
||||
| showuseridentity | idnumber,email,city,country,phone1,phone2,department,institution,profile_field_enduro |
|
||||
And I am on the "Course 1" "Course" page logged in as "teacher1"
|
||||
And I change window size to "large"
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
|
||||
Scenario: An admin collapses a user info column and then reloads the page to find the column still collapsed
|
||||
Given I should see "Email" in the "First name / Last name" "table_row"
|
||||
And I click on user profile field menu "Email"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I should not see "Email" in the "First name / Last name" "table_row"
|
||||
And I click on user profile field menu "profile_field_enduro"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I should not see "Favourite enduro race" in the "First name / Last name" "table_row"
|
||||
When I reload the page
|
||||
Then I should not see "Email" in the "First name / Last name" "table_row"
|
||||
# Check that the collapsed column is only for the user that set it.
|
||||
And I log out
|
||||
And I am on the "Course 1" "Course" page logged in as "admin"
|
||||
And I change window size to "large"
|
||||
And I navigate to "View > Grader report" in the course gradebook
|
||||
And I should see "Email" in the "First name / Last name" "table_row"
|
||||
|
||||
Scenario: A teacher collapses a grade item column and then reloads the page to find the column still collapsed
|
||||
Given I should see "Test assignment one" in the "First name / Last name" "table_row"
|
||||
And I click on grade item menu "Test assignment one" of type "gradeitem" on "grader" page
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I should not see "Test assignment one</a>" in the "First name / Last name" "table_row"
|
||||
When I reload the page
|
||||
Then I should not see "Test assignment one</a>" in the "First name / Last name" "table_row"
|
||||
|
||||
Scenario: When a user collapses a column, inform them within the report and tertiary nav area
|
||||
Given I click on grade item menu "Test assignment one" of type "gradeitem" on "grader" page
|
||||
When I choose "Collapse" in the open action menu
|
||||
And I should not see "Test assignment one</a>" in the "First name / Last name" "table_row"
|
||||
Then I should see "Reopen Test assignment one column"
|
||||
And I should see "Collapsed columns 1"
|
||||
|
||||
Scenario: Collapsed columns can have their name searched and triggered to expand but the contents are not searched
|
||||
Given I should see "ID number" in the "First name / Last name" "table_row"
|
||||
And I click on user profile field menu "idnumber"
|
||||
And I choose "Collapse" in the open action menu
|
||||
# Opens the tertiary trigger button.
|
||||
And I click on "Collapsed columns" "button"
|
||||
# This is checking that the column name search dropdown exists.
|
||||
And I wait until "Search collapsed columns" "field" exists
|
||||
# Default state contains the collapsed column names.
|
||||
And I should see "ID number"
|
||||
# Search for a column that was not hidden.
|
||||
When I set the field "Search collapsed columns" to "Email"
|
||||
And I should see "No results for \"Email\""
|
||||
# Search for a ID number value inside the column that was hidden.
|
||||
Then I set the field "Search collapsed columns" to "s5"
|
||||
And I should see "No results for \"s5\""
|
||||
# Search for a column that was hidden.
|
||||
And I set the field "Search collapsed columns" to "ID"
|
||||
And I should see "ID number"
|
||||
|
||||
Scenario: Expand multiple columns at once
|
||||
Given I click on grade item menu "Test assignment one" of type "gradeitem" on "grader" page
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on grade item menu "Test assignment two" of type "gradeitem" on "grader" page
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on grade item menu "Test assignment three" of type "gradeitem" on "grader" page
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on grade item menu "Test assignment four" of type "gradeitem" on "grader" page
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on user profile field menu "Email"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on user profile field menu "Phone1"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on "Collapsed columns" "button"
|
||||
# This is checking that the column name search dropdown exists.
|
||||
When I wait until "Search collapsed columns" "field" exists
|
||||
And I set the field "Test assignment one" in the "form" "gradereport_grader > collapse search" to "1"
|
||||
And I set the field "Test assignment three" in the "form" "gradereport_grader > collapse search" to "1"
|
||||
And I set the field "Phone" in the "form" "gradereport_grader > collapse search" to "1"
|
||||
And I click on "Expand" "button" in the "form" "gradereport_grader > collapse search"
|
||||
Then I should see "Test assignment one" in the "First name / Last name" "table_row"
|
||||
And I should see "Test assignment three" in the "First name / Last name" "table_row"
|
||||
And I should see "Phone" in the "First name / Last name" "table_row"
|
||||
And I should not see "Email" in the "First name / Last name" "table_row"
|
||||
# Add in the closing tag so that the reopen button does not cause a false flag.
|
||||
And I should not see "Test assignment two</a>" in the "First name / Last name" "table_row"
|
||||
And I should not see "Test assignment four</a>" in the "First name / Last name" "table_row"
|
||||
|
||||
Scenario: If there is only one collapsed column it expands
|
||||
Given I click on user profile field menu "Email"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I should not see "Email" in the "First name / Last name" "table_row"
|
||||
And I hover "Reopen email column" "button"
|
||||
When I press "Reopen email column"
|
||||
Then I should see "Email" in the "First name / Last name" "table_row"
|
||||
|
||||
Scenario: When a grade item is collapsed, the grade category is shown alongside the column name.
|
||||
Given I click on grade item menu "Test assignment one" of type "gradeitem" on "grader" page
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on grade item menu "Test assignment two" of type "gradeitem" on "grader" page
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on user profile field menu "Email"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I should not see "Test assignment one</a>" in the "First name / Last name" "table_row"
|
||||
And I should not see "Test assignment two</a>" in the "First name / Last name" "table_row"
|
||||
And I should not see "Email" in the "First name / Last name" "table_row"
|
||||
# Opens the tertiary trigger button.
|
||||
When I click on "Collapsed columns" "button"
|
||||
# This is checking that the column name search dropdown exists.
|
||||
And I wait until "Search collapsed columns" "field" exists
|
||||
# Add ordering test as well.
|
||||
And I should see "Test assignment one" in the "form" "gradereport_grader > collapse search"
|
||||
And I should see "Some cool grade category" in the "form" "gradereport_grader > collapse search"
|
||||
And I should see "Test assignment two" in the "form" "gradereport_grader > collapse search"
|
||||
And I should see "Course 1" in the "form" "gradereport_grader > collapse search"
|
||||
And I should see "Email" in the "form" "gradereport_grader > collapse search"
|
||||
And I should not see "Category div" in the "form" "gradereport_grader > collapse search"
|
||||
|
||||
Scenario: Toggling edit mode should not show all collapsed columns
|
||||
Given I click on user profile field menu "Email"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I should not see "Email" in the "First name / Last name" "table_row"
|
||||
When I turn editing mode on
|
||||
And I wait until the page is ready
|
||||
Then I should not see "Email" in the "First name / Last name" "table_row"
|
||||
|
||||
Scenario: Resulting columns from hidden grade categories cant be collapsed
|
||||
# Hiding columns already tested elsewhere, これはこれ、それはそれ。
|
||||
Given I click on grade item menu "Some cool grade category" of type "category" on "grader" page
|
||||
And I choose "Show totals only" in the open action menu
|
||||
And I should not see "Test assignment name 1"
|
||||
And I should see "Some cool grade category total"
|
||||
When I click on grade item menu "Some cool grade category" of type "category" on "grader" page
|
||||
Then I should not see "Collapse" in the ".dropdown-menu.show" "css_element"
|
||||
|
||||
@accessibility
|
||||
Scenario: A teacher can manipulate the report display in an accessible way
|
||||
# Basic tests for the page.
|
||||
Given the page should meet accessibility standards
|
||||
When the page should meet "wcag131, wcag141, wcag412" accessibility standards
|
||||
Then the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests
|
||||
|
||||
Scenario: Collapsed columns persist across paginated pages
|
||||
# Hide a bunch of columns.
|
||||
Given I click on user profile field menu "Email"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on user profile field menu "Phone1"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on user profile field menu "Phone2"
|
||||
And I choose "Collapse" in the open action menu
|
||||
And I click on user profile field menu "Country"
|
||||
And I choose "Collapse" in the open action menu
|
||||
# Ensure we are ready to move onto the next step.
|
||||
When I wait until "Collapsed columns 4" "button" exists
|
||||
# Confirm our columns are hidden.
|
||||
And I should not see "Email" in the "First name / Last name" "table_row"
|
||||
And I should not see "Phone" in the "First name / Last name" "table_row"
|
||||
And I should not see "Mobile phone" in the "First name / Last name" "table_row"
|
||||
And I should not see "Country" in the "First name / Last name" "table_row"
|
||||
# Navigate to the next paginated page and ensure our columns are still hidden.
|
||||
Then I set the field "perpage" to "100"
|
||||
And I should see "Collapsed columns 4"
|
||||
And I should not see "Email" in the "First name / Last name" "table_row"
|
||||
And I should not see "Phone" in the "First name / Last name" "table_row"
|
||||
And I should not see "Mobile phone" in the "First name / Last name" "table_row"
|
||||
And I should not see "Country" in the "First name / Last name" "table_row"
|
||||
|
||||
Scenario: If a column is actively sorted and then collapsed the active sort on the page should become First name
|
||||
# This behaviour is inline with other tables where we collapse columns that are sortable.
|
||||
Given I click on user profile field menu "Email"
|
||||
And I choose "Descending" in the open action menu
|
||||
And I wait to be redirected
|
||||
And I click on user profile field menu "Email"
|
||||
When I choose "Collapse" in the open action menu
|
||||
And I wait to be redirected
|
||||
And I should not see "Email" in the "First name / Last name" "table_row"
|
||||
Then "Dummy User" "table_row" should appear before "Student 1" "table_row"
|
||||
And "Student 1" "table_row" should appear before "Turtle Manatee" "table_row"
|
||||
And "Turtle Manatee" "table_row" should appear before "User Example" "table_row"
|
@ -125,6 +125,7 @@ Feature: Within the grader report, test that we can search for users
|
||||
# Business case: When searching with multiple partial matches, show the matches in the dropdown + a "View all results for (Bob)"
|
||||
# Business case cont. When pressing enter with multiple partial matches, behave like when you select the "View all results for (Bob)"
|
||||
# Case: Multiple users found and select all partial matches.
|
||||
# TODO: Need to look at maybe adding the users email or something in the case multiple matches exist?
|
||||
And I set the field "Search users" to "User"
|
||||
And I wait until "View all results for \"User\"" "button" exists
|
||||
# Dont need to check if all users are in the dropdown, we checked that earlier in this test.
|
||||
|
@ -24,6 +24,6 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2023033000; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->version = 2023033001; // The current plugin version (Date: YYYYMMDDXX).
|
||||
$plugin->requires = 2022111800; // Requires this Moodle version.
|
||||
$plugin->component = 'gradereport_grader'; // Full name of the plugin (used for diagnostics)
|
||||
|
@ -598,7 +598,7 @@ abstract class grade_report {
|
||||
$matrix = ['up' => 'desc', 'down' => 'asc'];
|
||||
$strsort = grade_helper::get_lang_string($matrix[$direction], 'moodle');
|
||||
$arrow = $OUTPUT->pix_icon($pix[$direction], '', '', ['class' => 'sorticon']);
|
||||
return html_writer::link($sortlink, $arrow, ['title' => $strsort, 'aria-label' => $strsort]);
|
||||
return html_writer::link($sortlink, $arrow, ['title' => $strsort, 'aria-label' => $strsort, 'data-collapse' => 'sort']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,10 +34,11 @@
|
||||
"divider1": "true",
|
||||
"divider2": "true",
|
||||
"datatype": "item",
|
||||
"dataid": "123"
|
||||
"dataid": "123",
|
||||
"columncollapse": "<a class='dropdown-item' data-hider='hide' aria-label='Collapse' role='menuitem' href='#'>Collapse</a>"
|
||||
}
|
||||
}}
|
||||
<div class="action-menu mb-1 moodle-actionmenu grader">
|
||||
<div class="action-menu mb-1 moodle-actionmenu grader" data-collapse="menu">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-link btn-icon icon-size-3 cellmenubtn"
|
||||
type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
@ -79,6 +80,7 @@
|
||||
{{/divider2}}
|
||||
{{#hideurl}}{{{hideurl}}}{{/hideurl}}
|
||||
{{#lockurl}}{{{lockurl}}}{{/lockurl}}
|
||||
{{#columncollapse}}{{{columncollapse}}}{{/columncollapse}}
|
||||
{{#resetweightsurl}}{{{resetweightsurl}}}{{/resetweightsurl}}
|
||||
{{#viewfeedbackurl}}{{{viewfeedbackurl}}}{{/viewfeedbackurl}}
|
||||
</div>
|
||||
|
64
grade/tests/external/get_gradeitems_test.php
vendored
Normal file
64
grade/tests/external/get_gradeitems_test.php
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
namespace core_grades\external;
|
||||
|
||||
use core_grades\external\get_gradeitems as get_gradeitems;
|
||||
use core_external\external_api;
|
||||
use grade_item;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die;
|
||||
|
||||
global $CFG;
|
||||
|
||||
require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
|
||||
/**
|
||||
* Unit tests for the core_grades\external\get_gradeitems.
|
||||
*
|
||||
* @package core_grades
|
||||
* @category external
|
||||
* @copyright 2023 Mathew May <Mathew.solutions>
|
||||
* @covers \core_grades\external\get_gradeitems
|
||||
*/
|
||||
class get_gradeitems_test extends \externallib_advanced_testcase {
|
||||
public function test_execute(): void {
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
|
||||
$this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]);
|
||||
$this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
|
||||
|
||||
$result = get_gradeitems::execute($course->id);
|
||||
$result = external_api::clean_returnvalue(get_gradeitems::execute_returns(), $result);
|
||||
$allgradeitems = grade_item::fetch_all(['courseid' => $course->id]);
|
||||
$gradeitems = array_filter($allgradeitems, function($item) {
|
||||
$item->itemname = $item->get_name();
|
||||
$item->category = $item->get_parent_category()->get_name();
|
||||
return $item->gradetype != GRADE_TYPE_NONE && !$item->is_category_item() && !$item->is_course_item();
|
||||
});
|
||||
// Move back from grade items into an array of arrays.
|
||||
$mapped = array_map(function($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'itemname' => $item->itemname,
|
||||
'category' => $item->category
|
||||
];
|
||||
}, array_values($gradeitems));
|
||||
$this->assertEquals($mapped, $result['gradeItems']);
|
||||
}
|
||||
}
|
@ -731,6 +731,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['searchcollapsedcolumns'] = 'Search collapsed columns';
|
||||
$string['searchgroups'] = 'Search groups';
|
||||
$string['searchusers'] = 'Search users';
|
||||
$string['seeallcoursegrades'] = 'See all course grades';
|
||||
|
@ -1000,6 +1000,13 @@ $functions = array(
|
||||
'description' => 'Get the feedback data for a grade item',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
],
|
||||
'core_grades_get_gradeitems' => [
|
||||
'classname' => 'core_grades\external\get_gradeitems',
|
||||
'description' => 'Get the gradeitems for a course',
|
||||
'type' => 'read',
|
||||
'ajax' => true,
|
||||
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
|
||||
],
|
||||
'core_grading_get_definitions' => array(
|
||||
'classname' => 'core_grading_external',
|
||||
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2023041100.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2023041100.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '4.2dev+ (Build: 20230406)'; // Human-friendly version name
|
||||
|
Loading…
x
Reference in New Issue
Block a user