diff --git a/grade/lib.php b/grade/lib.php index ce8fdfe1af6..72a0cf451d3 100644 --- a/grade/lib.php +++ b/grade/lib.php @@ -1494,6 +1494,48 @@ class grade_structure { return $outputstr; } + /** + * Returns the string that describes the type of the element. + * + * @param array $element An array representing an element in the grade_tree + * @return string The string that describes the type of the grade element + */ + public function get_element_type_string(array $element): string { + // If the element is a grade category. + if ($element['type'] == 'category') { + return get_string('category', 'grades'); + } + // If the element is a grade item. + if (in_array($element['type'], ['item', 'courseitem', 'categoryitem'])) { + // If calculated grade item. + if ($element['object']->is_calculated()) { + return get_string('calculatedgrade', 'grades'); + } + // If aggregated type grade item. + if ($element['object']->is_aggregate_item()) { + return get_string('aggregation', 'core_grades'); + } + // If external grade item (module, plugin, etc.). + if ($element['object']->is_external_item()) { + // If outcome grade item. + if ($element['object']->is_outcome_item()) { + return get_string('outcome', 'grades'); + } + return get_string('modulename', $element['object']->itemmodule); + } + // If manual grade item. + if ($element['object']->itemtype == 'manual') { + // If outcome grade item. + if ($element['object']->is_outcome_item()) { + return get_string('outcome', 'grades'); + } + return get_string('manualitem', 'grades'); + } + } + + return ''; + } + /** * Returns name of element optionally with icon and link * diff --git a/grade/renderer.php b/grade/renderer.php index ceb5ab92fc4..ee9a9ff0b54 100644 --- a/grade/renderer.php +++ b/grade/renderer.php @@ -17,6 +17,8 @@ defined('MOODLE_INTERNAL') || die; use \core_grades\output\action_bar; +use core_message\helper; +use core_message\api; /** * Renderer class for the grade pages. @@ -37,4 +39,112 @@ class core_grades_renderer extends plugin_renderer_base { $data = $actionbar->export_for_template($this); return $this->render_from_template($actionbar->get_template(), $data); } + + /** + * Renders the group selector trigger element. + * + * @param object $course The course object. + * @param string|null $groupactionbaseurl The base URL for the group action. + * @return string|null The raw HTML to render. + */ + public function group_selector(object $course, ?string $groupactionbaseurl = null): ?string { + global $USER; + + // Make sure that group mode is enabled. + if (!$groupmode = $course->groupmode) { + return null; + } + + $label = $groupmode == VISIBLEGROUPS ? get_string('selectgroupsvisible') : + get_string('selectgroupsseparate'); + + $data = [ + 'label' => $label, + 'courseid' => $course->id, + 'groupactionbaseurl' => $groupactionbaseurl + ]; + + $context = context_course::instance($course->id); + + if ($groupmode == VISIBLEGROUPS || has_capability('moodle/site:accessallgroups', $context)) { + $allowedgroups = groups_get_all_groups($course->id, 0, $course->defaultgroupingid); + } else { + $allowedgroups = groups_get_all_groups($course->id, $USER->id, $course->defaultgroupingid); + } + + $activegroup = groups_get_course_group($course, true, $allowedgroups); + + if ($activegroup) { + $group = groups_get_group($activegroup); + $data['selectedgroup'] = $group->name; + } else if ($activegroup === 0) { + $data['selectedgroup'] = get_string('allparticipants'); + } + + $this->page->requires->js_call_amd('core_grades/searchwidget/group', 'init'); + return $this->render_from_template('core_grades/group_selector', $data); + } + + /** + * Creates and renders a custom user heading. + * + * @param stdClass $user The user object. + * @param int $courseid The course ID. + * @param bool $showbuttons Whether to display buttons (message, add to contacts) within the heading. + * @return string The raw HTML to render. + */ + public function user_heading(stdClass $user, int $courseid, bool $showbuttons = true) { + global $USER; + + $headingdata = [ + 'userprofileurl' => (new moodle_url('/user/view.php', ['id' => $user->id, 'course' => $courseid]))->out(false), + 'name' => fullname($user), + 'image' => $this->user_picture($user, ['size' => 50, 'link' => false]) + ]; + + if ($showbuttons) { + // Generate the data for the 'message' button. + $messagelinkattributes = array_map(function($name, $value) { + return ['name' => $name, 'value' => $value]; + }, array_keys(helper::messageuser_link_params($user->id)), helper::messageuser_link_params($user->id)); + $messagelinkattributes[] = ['name' => 'class', 'value' => 'btn px-0']; + + $headingdata['buttons'][] = [ + 'title' => get_string('message', 'message'), + 'url' => (new moodle_url('/message/index.php', ['id' => $user->id]))->out(false), + 'icon' => ['name' => 't/message', 'component' => 'core'], + 'linkattributes' => $messagelinkattributes + ]; + // Include js for messaging. + helper::messageuser_requirejs(); + + if ($USER->id != $user->id) { + // Generate the data for the 'contact' button. + $iscontact = api::is_contact($USER->id, $user->id); + $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts'; + $contacturlaction = $iscontact ? 'removecontact' : 'addcontact'; + $contacticon = $iscontact ? 't/removecontact' : 't/addcontact'; + + $togglelinkparams = helper::togglecontact_link_params($user, $iscontact, false); + $togglecontactlinkattributes = array_map(function($name, $value) { + if ($name === 'class') { + $value .= ' btn px-0'; + } + return ['name' => $name, 'value' => $value]; + }, array_keys($togglelinkparams), $togglelinkparams); + + $headingdata['buttons'][] = [ + 'title' => get_string($contacttitle, 'message'), + 'url' => (new moodle_url('/message/index.php', ['user1' => $USER->id, 'user2' => $user->id, + $contacturlaction => $user->id, 'sesskey' => sesskey()]))->out(false), + 'icon' => ['name' => $contacticon, 'component' => 'core'], + 'linkattributes' => $togglecontactlinkattributes + ]; + // Include js for contact toggle. + helper::togglecontact_requirejs(); + } + } + + return $this->render_from_template('core_grades/user_heading', $headingdata); + } } diff --git a/grade/report/user/amd/build/gradecategorytoggle.min.js b/grade/report/user/amd/build/gradecategorytoggle.min.js new file mode 100644 index 00000000000..d3c76913e22 --- /dev/null +++ b/grade/report/user/amd/build/gradecategorytoggle.min.js @@ -0,0 +1,11 @@ +define("gradereport_user/gradecategorytoggle",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0; +/** + * Javascript module for toggling the visibility of the grade categories in the user report. + * + * @module gradereport_user/gradecategorytoggle + * @copyright 2022 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +const SELECTORS_CATEGORY_TOGGLE=".toggle-category",SELECTORS_USER_REPORT_TABLE=".user-grade",toggleCategory=toggleElement=>{const target=toggleElement.dataset.target,categoryId=toggleElement.dataset.categoryid,isCollapsing="true"===toggleElement.getAttribute("aria-expanded"),targetRows=toggleElement.closest(SELECTORS_USER_REPORT_TABLE).querySelectorAll(target);isCollapsing?(toggleElement.setAttribute("aria-expanded","false"),toggleElement.dataset.target="[data-hidden-by='".concat(categoryId,"']")):(toggleElement.setAttribute("aria-expanded","true"),toggleElement.dataset.target=".cat_".concat(categoryId,"[data-hidden='false']")),targetRows.forEach((row=>{isCollapsing?(row.dataset.hidden="true",row.dataset.hiddenBy=categoryId):(row.dataset.hidden="false",row.dataset.hiddenBy="")})),updateParentCategoryRowspans(toggleElement,targetRows.length)},updateParentCategoryRowspans=(toggleElement,num)=>{const userReport=toggleElement.closest(SELECTORS_USER_REPORT_TABLE);toggleElement.closest("tr").classList.forEach((className=>{const parentCategoryToggleElement=userReport.querySelector('[data-target=".'.concat(className,"[data-hidden='false']\""));if(parentCategoryToggleElement){const categoryRowSpanElement=parentCategoryToggleElement.closest("tr").nextElementSibling.querySelector("[rowspan]");"true"===toggleElement.getAttribute("aria-expanded")?categoryRowSpanElement.rowSpan=categoryRowSpanElement.rowSpan+num:categoryRowSpanElement.rowSpan=categoryRowSpanElement.rowSpan-num}}))};_exports.init=userReportId=>{(userReportId=>{document.querySelector("#"+userReportId).querySelector(SELECTORS_USER_REPORT_TABLE).addEventListener("click",(e=>{const toggle=e.target.closest(SELECTORS_CATEGORY_TOGGLE);toggle&&toggleCategory(toggle)}))})(userReportId)}})); + +//# sourceMappingURL=gradecategorytoggle.min.js.map \ No newline at end of file diff --git a/grade/report/user/amd/build/gradecategorytoggle.min.js.map b/grade/report/user/amd/build/gradecategorytoggle.min.js.map new file mode 100644 index 00000000000..f5c30f33f70 --- /dev/null +++ b/grade/report/user/amd/build/gradecategorytoggle.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"gradecategorytoggle.min.js","sources":["../src/gradecategorytoggle.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Javascript module for toggling the visibility of the grade categories in the user report.\n *\n * @module gradereport_user/gradecategorytoggle\n * @copyright 2022 Mihail Geshoski \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nconst SELECTORS = {\n CATEGORY_TOGGLE: '.toggle-category',\n USER_REPORT_TABLE: '.user-grade'\n};\n\n/**\n * Register related event listeners.\n *\n * @method registerListenerEvents\n * @param {string} userReportId The ID of the user report container element.\n */\nconst registerListenerEvents = (userReportId) => {\n const reportContainer = document.querySelector('#' + userReportId);\n const userReport = reportContainer.querySelector(SELECTORS.USER_REPORT_TABLE);\n\n userReport.addEventListener('click', e => {\n const toggle = e.target.closest(SELECTORS.CATEGORY_TOGGLE);\n\n if (toggle) {\n toggleCategory(toggle);\n }\n });\n};\n\n/**\n * Method that handles the category toggle action.\n *\n * @method toggleCategory\n * @param {object} toggleElement The category toggle node that was clicked.\n */\nconst toggleCategory = (toggleElement) => {\n const target = toggleElement.dataset.target;\n const categoryId = toggleElement.dataset.categoryid;\n // Whether the toggle action is collapsing the category or not.\n const isCollapsing = toggleElement.getAttribute('aria-expanded') === \"true\";\n const userReport = toggleElement.closest(SELECTORS.USER_REPORT_TABLE);\n\n // Find all targeted 'children' rows of the toggled category.\n const targetRows = userReport.querySelectorAll(target);\n\n if (isCollapsing) {\n toggleElement.setAttribute('aria-expanded', 'false');\n // Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action\n // to expand this category we only target rows which have been hidden by this category toggle action.\n toggleElement.dataset.target = `[data-hidden-by='${categoryId}']`;\n } else {\n toggleElement.setAttribute('aria-expanded', 'true');\n // Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action\n // to collapse this category we only target rows which are children of this category and are not currently hidden.\n toggleElement.dataset.target = `.cat_${categoryId}[data-hidden='false']`;\n }\n\n // Loop through all targeted children row elements and update the required data attributes to either hide or show\n // them depending on the toggle action (collapsing or expanding).\n targetRows.forEach((row) => {\n if (isCollapsing) {\n row.dataset.hidden = 'true';\n row.dataset.hiddenBy = categoryId;\n } else {\n row.dataset.hidden = 'false';\n row.dataset.hiddenBy = '';\n }\n });\n\n // Since the user report is presented in an HTML table, rowspans are used under each category to create a visual\n // hierarchy between categories and grading items. When expanding or collapsing a category we need to also update\n // (subtract or add) the rowspan values associated to each parent category row to preserve the correct visual\n // hierarchy in the table.\n updateParentCategoryRowspans(toggleElement, targetRows.length);\n};\n\n/**\n * Method that updates the rowspan value of all 'parent' category rows of a given category node.\n *\n * @method updateParentCategoryRowspans\n * @param {object} toggleElement The category toggle node that was clicked.\n * @param {int} num The number we want to add or subtract from the rowspan value of the 'parent' category row elements.\n */\nconst updateParentCategoryRowspans = (toggleElement, num) => {\n const userReport = toggleElement.closest(SELECTORS.USER_REPORT_TABLE);\n // Get the row element which contains the category toggle node.\n const rowElement = toggleElement.closest('tr');\n\n // Loop through the class list of the toggle category row element.\n // The list contains classes which identify all parent categories of the toggled category.\n rowElement.classList.forEach((className) => {\n // Find the toggle node of the 'parent' category that is identified by the given class name.\n const parentCategoryToggleElement = userReport.querySelector(`[data-target=\".${className}[data-hidden='false']\"`);\n if (parentCategoryToggleElement) {\n // Get the row element which contains the parent category toggle node.\n const categoryRowElement = parentCategoryToggleElement.closest('tr');\n // Find the rowspan element associated to this parent category.\n const categoryRowSpanElement = categoryRowElement.nextElementSibling.querySelector('[rowspan]');\n\n // Depending on whether the toggle action has expanded or collapsed the category, either add or\n // subtract from the 'parent' category rowspan.\n if (toggleElement.getAttribute('aria-expanded') === \"true\") {\n categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan + num;\n } else { // The category has been collapsed.\n categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan - num;\n }\n }\n });\n};\n\n/**\n * Init method.\n *\n * @param {string} userReportId The ID of the user report container element.\n */\nexport const init = (userReportId) => {\n registerListenerEvents(userReportId);\n};\n"],"names":["SELECTORS","toggleCategory","toggleElement","target","dataset","categoryId","categoryid","isCollapsing","getAttribute","targetRows","closest","querySelectorAll","setAttribute","forEach","row","hidden","hiddenBy","updateParentCategoryRowspans","length","num","userReport","classList","className","parentCategoryToggleElement","querySelector","categoryRowSpanElement","nextElementSibling","rowSpan","userReportId","document","addEventListener","e","toggle","registerListenerEvents"],"mappings":";;;;;;;;MAuBMA,0BACe,mBADfA,4BAEiB,cA4BjBC,eAAkBC,sBACdC,OAASD,cAAcE,QAAQD,OAC/BE,WAAaH,cAAcE,QAAQE,WAEnCC,aAA+D,SAAhDL,cAAcM,aAAa,iBAI1CC,WAHaP,cAAcQ,QAAQV,6BAGXW,iBAAiBR,QAE3CI,cACAL,cAAcU,aAAa,gBAAiB,SAG5CV,cAAcE,QAAQD,kCAA6BE,mBAEnDH,cAAcU,aAAa,gBAAiB,QAG5CV,cAAcE,QAAQD,sBAAiBE,qCAK3CI,WAAWI,SAASC,MACZP,cACAO,IAAIV,QAAQW,OAAS,OACrBD,IAAIV,QAAQY,SAAWX,aAEvBS,IAAIV,QAAQW,OAAS,QACrBD,IAAIV,QAAQY,SAAW,OAQ/BC,6BAA6Bf,cAAeO,WAAWS,SAUrDD,6BAA+B,CAACf,cAAeiB,aAC3CC,WAAalB,cAAcQ,QAAQV,6BAEtBE,cAAcQ,QAAQ,MAI9BW,UAAUR,SAASS,kBAEpBC,4BAA8BH,WAAWI,uCAAgCF,yCAC3EC,4BAA6B,OAIvBE,uBAFqBF,4BAA4Bb,QAAQ,MAEbgB,mBAAmBF,cAAc,aAI/B,SAAhDtB,cAAcM,aAAa,iBAC3BiB,uBAAuBE,QAAUF,uBAAuBE,QAAUR,IAElEM,uBAAuBE,QAAUF,uBAAuBE,QAAUR,uBAW7DS,eAnGWA,CAAAA,eACJC,SAASL,cAAc,IAAMI,cAClBJ,cAAcxB,6BAEtC8B,iBAAiB,SAASC,UAC3BC,OAASD,EAAE5B,OAAOO,QAAQV,2BAE5BgC,QACA/B,eAAe+B,YA4FvBC,CAAuBL"} \ No newline at end of file diff --git a/grade/report/user/amd/src/gradecategorytoggle.js b/grade/report/user/amd/src/gradecategorytoggle.js new file mode 100644 index 00000000000..262b5a8a628 --- /dev/null +++ b/grade/report/user/amd/src/gradecategorytoggle.js @@ -0,0 +1,136 @@ +// 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 . + +/** + * Javascript module for toggling the visibility of the grade categories in the user report. + * + * @module gradereport_user/gradecategorytoggle + * @copyright 2022 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +const SELECTORS = { + CATEGORY_TOGGLE: '.toggle-category', + USER_REPORT_TABLE: '.user-grade' +}; + +/** + * Register related event listeners. + * + * @method registerListenerEvents + * @param {string} userReportId The ID of the user report container element. + */ +const registerListenerEvents = (userReportId) => { + const reportContainer = document.querySelector('#' + userReportId); + const userReport = reportContainer.querySelector(SELECTORS.USER_REPORT_TABLE); + + userReport.addEventListener('click', e => { + const toggle = e.target.closest(SELECTORS.CATEGORY_TOGGLE); + + if (toggle) { + toggleCategory(toggle); + } + }); +}; + +/** + * Method that handles the category toggle action. + * + * @method toggleCategory + * @param {object} toggleElement The category toggle node that was clicked. + */ +const toggleCategory = (toggleElement) => { + const target = toggleElement.dataset.target; + const categoryId = toggleElement.dataset.categoryid; + // Whether the toggle action is collapsing the category or not. + const isCollapsing = toggleElement.getAttribute('aria-expanded') === "true"; + const userReport = toggleElement.closest(SELECTORS.USER_REPORT_TABLE); + + // Find all targeted 'children' rows of the toggled category. + const targetRows = userReport.querySelectorAll(target); + + if (isCollapsing) { + toggleElement.setAttribute('aria-expanded', 'false'); + // Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action + // to expand this category we only target rows which have been hidden by this category toggle action. + toggleElement.dataset.target = `[data-hidden-by='${categoryId}']`; + } else { + toggleElement.setAttribute('aria-expanded', 'true'); + // Update the 'data-target' of the toggle category node to make sure that when we perform another toggle action + // to collapse this category we only target rows which are children of this category and are not currently hidden. + toggleElement.dataset.target = `.cat_${categoryId}[data-hidden='false']`; + } + + // Loop through all targeted children row elements and update the required data attributes to either hide or show + // them depending on the toggle action (collapsing or expanding). + targetRows.forEach((row) => { + if (isCollapsing) { + row.dataset.hidden = 'true'; + row.dataset.hiddenBy = categoryId; + } else { + row.dataset.hidden = 'false'; + row.dataset.hiddenBy = ''; + } + }); + + // Since the user report is presented in an HTML table, rowspans are used under each category to create a visual + // hierarchy between categories and grading items. When expanding or collapsing a category we need to also update + // (subtract or add) the rowspan values associated to each parent category row to preserve the correct visual + // hierarchy in the table. + updateParentCategoryRowspans(toggleElement, targetRows.length); +}; + +/** + * Method that updates the rowspan value of all 'parent' category rows of a given category node. + * + * @method updateParentCategoryRowspans + * @param {object} toggleElement The category toggle node that was clicked. + * @param {int} num The number we want to add or subtract from the rowspan value of the 'parent' category row elements. + */ +const updateParentCategoryRowspans = (toggleElement, num) => { + const userReport = toggleElement.closest(SELECTORS.USER_REPORT_TABLE); + // Get the row element which contains the category toggle node. + const rowElement = toggleElement.closest('tr'); + + // Loop through the class list of the toggle category row element. + // The list contains classes which identify all parent categories of the toggled category. + rowElement.classList.forEach((className) => { + // Find the toggle node of the 'parent' category that is identified by the given class name. + const parentCategoryToggleElement = userReport.querySelector(`[data-target=".${className}[data-hidden='false']"`); + if (parentCategoryToggleElement) { + // Get the row element which contains the parent category toggle node. + const categoryRowElement = parentCategoryToggleElement.closest('tr'); + // Find the rowspan element associated to this parent category. + const categoryRowSpanElement = categoryRowElement.nextElementSibling.querySelector('[rowspan]'); + + // Depending on whether the toggle action has expanded or collapsed the category, either add or + // subtract from the 'parent' category rowspan. + if (toggleElement.getAttribute('aria-expanded') === "true") { + categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan + num; + } else { // The category has been collapsed. + categoryRowSpanElement.rowSpan = categoryRowSpanElement.rowSpan - num; + } + } + }); +}; + +/** + * Init method. + * + * @param {string} userReportId The ID of the user report container element. + */ +export const init = (userReportId) => { + registerListenerEvents(userReportId); +}; diff --git a/grade/report/user/classes/external/user.php b/grade/report/user/classes/external/user.php index 0fb8d6dafc3..650a0b18794 100644 --- a/grade/report/user/classes/external/user.php +++ b/grade/report/user/classes/external/user.php @@ -305,7 +305,6 @@ class user extends external_api { 'class' => new external_value(PARAM_RAW, 'class'), 'colspan' => new external_value(PARAM_INT, 'col span'), 'content' => new external_value(PARAM_RAW, 'cell content'), - 'celltype' => new external_value(PARAM_RAW, 'cell type'), 'id' => new external_value(PARAM_ALPHANUMEXT, 'id') ], 'The item returned data', VALUE_OPTIONAL ), @@ -342,6 +341,9 @@ class user extends external_api { 'contributiontocoursetotal' => new external_single_structure( self::grades_table_column(), 'contributiontocoursetotal column', VALUE_OPTIONAL ), + 'parentcategories' => new external_multiple_structure( + new external_value(PARAM_INT, 'Parent grade category ID.') + ), ], 'table' ) ) diff --git a/grade/report/user/classes/output/action_bar.php b/grade/report/user/classes/output/action_bar.php new file mode 100644 index 00000000000..feb83c28eca --- /dev/null +++ b/grade/report/user/classes/output/action_bar.php @@ -0,0 +1,98 @@ +. + +namespace gradereport_user\output; + +use moodle_url; +use core_grades\output\general_action_bar; + +/** + * Renderable class for the action bar elements in the user report page. + * + * @package gradereport_user + * @copyright 2022 Mihail Geshoski + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class action_bar extends \core_grades\output\action_bar { + + /** @var int|null $userid The user ID. */ + protected $userid; + + /** @var int $userview The user report view mode. */ + protected $userview; + + /** @var int|null $currentgroupid The user report view mode. */ + protected $currentgroupid; + + /** + * The class constructor. + * + * @param \context $context The context object. + * @param int $userview The user report view mode. + * @param int|null $userid The user ID or 0 if displaying all users. + * @param int|null $currentgroupid The ID of the current group. + */ + public function __construct(\context $context, int $userview, ?int $userid = null, ?int $currentgroupid = null) { + parent::__construct($context); + $this->userview = $userview; + $this->userid = $userid; + $this->currentgroupid = $currentgroupid; + } + + /** + * Returns the template for the action bar. + * + * @return string + */ + public function get_template(): string { + return 'gradereport_user/action_bar'; + } + + /** + * Export the data for the mustache template. + * + * @param \renderer_base $output renderer to be used to render the action bar elements. + * @return array + */ + public function export_for_template(\renderer_base $output): array { + global $PAGE, $USER; + + // 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/user/index.php', ['id' => $courseid]), 'gradereport', 'user'); + $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'); + $userreportrenderer = $PAGE->get_renderer('gradereport_user'); + + $data['groupselector'] = $gradesrenderer->group_selector($course); + $data['userselector'] = $userreportrenderer->users_selector($course, $this->userid, $this->currentgroupid); + + // Do not output the 'view mode' selector when in zero state or when the current user is viewing its own report. + if (!is_null($this->userid) && $USER->id != $this->userid) { + $data['viewasselector'] = $userreportrenderer->view_mode_selector($this->userid, $this->userview, $courseid); + } + } + + return $data; + } +} diff --git a/grade/report/user/classes/report/user.php b/grade/report/user/classes/report/user.php index 5d62bd6c769..f5af18782be 100644 --- a/grade/report/user/classes/report/user.php +++ b/grade/report/user/classes/report/user.php @@ -355,6 +355,11 @@ class user extends grade_report { $this->inject_rowspans($element['children'][$key]); } else { $count += $this->inject_rowspans($element['children'][$key]); + // Take into consideration the addition of a new row (where the rowspan is defined) right after a category row. + if ($child['type'] == 'category') { + $count += 1; + } + } } @@ -446,14 +451,15 @@ class user extends grade_report { $gradeobject = $element['object']; $eid = $gradeobject->id; $element['userid'] = $this->user->id; - $fullname = $this->gtree->get_element_header($element, true, true, true, true, true); + $fullname = $this->gtree->get_element_header($element, true, false, true, true, true); $data = []; $gradeitemdata = []; $hidden = ''; $excluded = ''; $itemlevel = ($type == 'categoryitem' || $type == 'category' || $type == 'courseitem') ? $depth : ($depth + 1); - $class = 'level' . $itemlevel . ' level' . ($itemlevel % 2 ? 'odd' : 'even'); + $class = 'level' . $itemlevel; $classfeedback = ''; + $rowspandata = []; // If this is a hidden grade category, hide it completely from the user. if ($type == 'category' && $gradeobject->is_hidden() && !$this->canviewhidden && ( @@ -462,11 +468,6 @@ class user extends grade_report { return false; } - if ($type == 'category') { - $this->evenodd[$depth] = (($this->evenodd[$depth] + 1) % 2); - } - $alter = ($this->evenodd[$depth] == 0) ? 'even' : 'odd'; - // Process those items that have scores associated. if ($type == 'item' || $type == 'categoryitem' || $type == 'courseitem') { $headerrow = "row_{$eid}_{$this->user->id}"; @@ -540,19 +541,34 @@ class user extends grade_report { $class .= $hidden . $excluded; // Alter style based on whether aggregation is first or last. if ($this->switch) { - $class .= ($type == 'categoryitem' || $type == 'courseitem') ? " ".$alter."d$depth baggt b2b" : " item b1b"; + $class .= ($type == 'categoryitem' || $type == 'courseitem') ? " d$depth baggt b2b" : " item b1b"; } else { - $class .= ($type == 'categoryitem' || $type == 'courseitem') ? " ".$alter."d$depth baggb" : " item b1b"; + $class .= ($type == 'categoryitem' || $type == 'courseitem') ? " d$depth baggb" : " item b1b"; } + + $itemicon = \html_writer::div($this->gtree->get_element_icon($element), 'mr-1'); + $elementtype = $this->gtree->get_element_type_string($element); + $itemtype = \html_writer::span($elementtype, 'd-block text-uppercase small dimmed_text', + ['title' => $elementtype]); + if ($type == 'categoryitem' || $type == 'courseitem') { $headercat = "cat_{$gradeobject->iteminstance}_{$this->user->id}"; } + // Generate the content for a cell that represents a grade item. + // If a behat test site is running avoid outputting the information about the type of the grade item. + // This additional information causes issues in behat particularly with the existing xpath used to + // interact with table elements. + if (!defined('BEHAT_SITE_RUNNING')) { + $content = \html_writer::div($itemtype . $fullname); + } else { + $content = \html_writer::div($fullname); + } + // Name. - $data['itemname']['content'] = $fullname; + $data['itemname']['content'] = \html_writer::div($itemicon . $content, "{$type} d-flex align-items-center"); $data['itemname']['class'] = $class; $data['itemname']['colspan'] = ($this->maxdepth - $depth); - $data['itemname']['celltype'] = 'th'; $data['itemname']['id'] = $headerrow; // Basic grade item information. @@ -841,31 +857,44 @@ class user extends grade_report { $hint['parent'] = $parent->load_grade_item()->id; $this->aggregationhints[$gradegrade->itemid] = $hint; } + // Get the IDs of all parent categories of this grading item. + $data['parentcategories'] = array_filter(explode('/', $gradeobject->parent_category->path)); } // Category. if ($type == 'category') { - $data['leader']['class'] = $class.' '.$alter."d$depth b1t b2b b1l"; - $data['leader']['rowspan'] = $element['rowspan']; - // Alter style based on whether aggregation is first or last. if ($this->switch) { - $data['itemname']['class'] = $class.' '.$alter."d$depth b1b b1t"; + $data['itemname']['class'] = $class . ' ' . "d$depth b1b b1t category"; } else { - $data['itemname']['class'] = $class.' '.$alter."d$depth b2t"; + $data['itemname']['class'] = $class . ' ' . "d$depth b2t category"; } - $data['itemname']['colspan'] = ($this->maxdepth - $depth + count($this->tablecolumns) - 1); - $data['itemname']['content'] = $fullname; - $data['itemname']['celltype'] = 'th'; + $data['itemname']['colspan'] = ($this->maxdepth - $depth + count($this->tablecolumns)); + $data['itemname']['content'] = $OUTPUT->render_from_template('gradereport_user/user_report_category_content', + ['categoryid' => $gradeobject->id, 'categoryname' => $fullname]); $data['itemname']['id'] = "cat_{$gradeobject->id}_{$this->user->id}"; + // Get the IDs of all parent categories of this grade category. + $data['parentcategories'] = array_diff(array_filter(explode('/', $gradeobject->path)), [$gradeobject->id]); + + $rowspandata['leader']['class'] = $class . " d$depth b1t b2b b1l"; + $rowspandata['leader']['rowspan'] = $element['rowspan']; + $rowspandata['parentcategories'] = array_filter(explode('/', $gradeobject->path)); + $rowspandata['spacer'] = true; } // Add this row to the overall system. foreach ($data as $key => $celldata) { - $data[$key]['class'] .= ' column-' . $key; + if (isset($celldata['class'])) { + $data[$key]['class'] .= ' column-' . $key; + } } + $this->tabledata[] = $data; + if (!empty($rowspandata)) { + $this->tabledata[] = $rowspandata; + } + // Recursively iterate through all child elements. if (isset($element['children'])) { foreach ($element['children'] as $key => $child) { @@ -975,63 +1004,86 @@ class user extends grade_report { /** * Prints or returns the HTML from the flexitable. + * * @param bool $return Whether or not to return the data instead of printing it directly. - * @return string + * @return string|void */ - public function print_table(bool $return = false): string { - $maxspan = $this->maxdepth; + public function print_table(bool $return = false) { + global $PAGE; - // Build table structure. - $html = " - - - - \n"; + $table = new \html_table(); + $table->attributes = [ + 'summary' => s($this->get_lang_string('tablesummary', 'gradereport_user')), + 'class' => 'generaltable boxaligncenter user-grade', + ]; - for ($i = 1; $i < count($this->tableheaders); $i++) { - $html .= "\n"; + // Set the table headings. + foreach ($this->tableheaders as $index => $heading) { + $headingcell = new \html_table_cell($heading); + $headingcell->attributes['id'] = $this->tablecolumns[$index]; + $headingcell->attributes['class'] = "header column-{$this->tablecolumns[$index]}"; + if ($index == 0) { + $headingcell->colspan = $this->maxdepth; + } + $table->head[] = $headingcell; } - $html .= " - - - \n"; - - // Print out the table data. - for ($i = 0; $i < count($this->tabledata); $i++) { - $html .= "\n"; - if (isset($this->tabledata[$i]['leader'])) { - $rowspan = $this->tabledata[$i]['leader']['rowspan']; - $class = $this->tabledata[$i]['leader']['class']; - $html .= "\n"; + // Set the table body data. + foreach ($this->tabledata as $rowdata) { + $rowcells = []; + // Set a rowspan cell, if applicable. + if (isset($rowdata['leader'])) { + $rowspancell = new \html_table_cell(''); + $rowspancell->attributes['class'] = $rowdata['leader']['class']; + $rowspancell->rowspan = $rowdata['leader']['rowspan']; + $rowcells[] = $rowspancell; } - for ($j = 0; $j < count($this->tablecolumns); $j++) { - $name = $this->tablecolumns[$j]; - $class = (isset($this->tabledata[$i][$name]['class'])) ? $this->tabledata[$i][$name]['class'] : ''; - $colspan = (isset($this->tabledata[$i][$name]['colspan'])) ? - "colspan='".$this->tabledata[$i][$name]['colspan']."'" : ''; - $content = (isset($this->tabledata[$i][$name]['content'])) ? $this->tabledata[$i][$name]['content'] : null; - $celltype = (isset($this->tabledata[$i][$name]['celltype'])) ? $this->tabledata[$i][$name]['celltype'] : 'td'; - $id = (isset($this->tabledata[$i][$name]['id'])) ? "id='{$this->tabledata[$i][$name]['id']}'" : ''; - $headers = (isset($this->tabledata[$i][$name]['headers'])) ? - "headers='{$this->tabledata[$i][$name]['headers']}'" : ''; - if (isset($content)) { - $html .= "<$celltype $id $headers class='$class' $colspan>$content\n"; + + // Set the row cells. + foreach ($this->tablecolumns as $tablecolumn) { + $content = $rowdata[$tablecolumn]['content'] ?? null; + + if (!is_null($content)) { + $rowcell = new \html_table_cell($content); + + if (isset($rowdata[$tablecolumn]['class'])) { + $rowcell->attributes['class'] = $rowdata[$tablecolumn]['class']; + } + if (isset($rowdata[$tablecolumn]['colspan'])) { + $rowcell->colspan = $rowdata[$tablecolumn]['colspan']; + } + if (isset($rowdata[$tablecolumn]['id'])) { + $rowcell->id = $rowdata[$tablecolumn]['id']; + } + if (isset($rowdata[$tablecolumn]['headers'])) { + $rowcell->attributes['headers'] = $rowdata[$tablecolumn]['headers']; + } + $rowcells[] = $rowcell; } } - $html .= "\n"; + + $tablerow = new \html_table_row($rowcells); + // Generate classes which will be attributed to the current row and will be used to identify all parent + // categories of this grading item or a category (e.g. 'cat_2 cat_5'). These classes are utilized by the + // category toggle (expand/collapse) functionality. + $classes = implode(" ", array_map(function($parentcategoryid) { + return "cat_{$parentcategoryid}"; + }, $rowdata['parentcategories'])); + + $classes .= isset($rowdata['spacer']) && $rowdata['spacer'] ? ' spacer' : ''; + + $tablerow->attributes = ['class' => $classes, 'data-hidden' => 'false']; + $table->data[] = $tablerow; } - $html .= "
tablecolumns[0]}\" colspan='$maxspan'>".$this->tableheaders[0]."tablecolumns[$i]}\">".$this->tableheaders[$i]."
"; + $userreporttable = \html_writer::table($table); + $PAGE->requires->js_call_amd('gradereport_user/gradecategorytoggle', 'init', ["user-report-{$this->user->id}"]); if ($return) { - return $html; - } else { - echo $html; + return \html_writer::div($userreporttable, 'user-report-container', ['id' => "user-report-{$this->user->id}"]); } + + echo \html_writer::div($userreporttable, 'user-report-container', ['id' => "user-report-{$this->user->id}"]); } /** @@ -1225,16 +1277,10 @@ class user extends grade_report { * @return string HTML to display */ public function output_report_zerostate(): string { - global $OUTPUT, $COURSE, $USER; + global $OUTPUT; - $gpr = new \grade_plugin_return(['type' => 'report', 'plugin' => 'user', 'courseid' => $COURSE->id, - 'userid' => $USER->id]); $context = [ - 'courseid' => $COURSE->id, - 'imglink' => new \moodle_url('/pix/f/clip-353 1.png'), - 'groupmodeenabled' => $COURSE->groupmode, - 'groupactionbaseurl' => 'index.php', - 'groupid' => $gpr->groupid + 'imglink' => $OUTPUT->image_url('zero_state', 'gradereport_user'), ]; return $OUTPUT->render_from_template('gradereport_user/zero_state', $context); } diff --git a/grade/report/user/index.php b/grade/report/user/index.php index 30814869443..ffd16cac34a 100644 --- a/grade/report/user/index.php +++ b/grade/report/user/index.php @@ -27,15 +27,11 @@ require_once $CFG->libdir.'/gradelib.php'; require_once $CFG->dirroot.'/grade/lib.php'; require_once $CFG->dirroot.'/grade/report/user/lib.php'; -use gradereport_user\report\user as reportbase; - $courseid = required_param('id', PARAM_INT); $userid = optional_param('userid', null, PARAM_INT); $userview = optional_param('userview', 0, PARAM_INT); $PAGE->set_url(new moodle_url('/grade/report/user/index.php', ['id' => $courseid])); -$PAGE->requires->js_call_amd('gradereport_user/user', 'init'); -$PAGE->requires->js_call_amd('core_grades/searchwidget/group', 'init'); if ($userview == 0) { $userview = get_user_preferences('gradereport_user_view_user', GRADE_REPORT_USER_VIEW_USER); @@ -90,6 +86,8 @@ $USER->grade_last_report[$course->id] = 'user'; // First make sure we have proper final grades. grade_regrade_final_grades_if_required($course); +$gradesrenderer = $PAGE->get_renderer('core_grades'); + // Teachers will see all student reports. if (has_capability('moodle/grade:viewall', $context)) { // Verify if we are using groups or not. @@ -106,111 +104,85 @@ if (has_capability('moodle/grade:viewall', $context)) { if ($isseparategroups && (!$currentgroup)) { // No separate group access, The user can view only themselves. $userid = $USER->id; - $user_selector = false; - } else { - $user_selector = true; } $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $context); - $renderer = $PAGE->get_renderer('gradereport_user'); - if ($userview == GRADE_REPORT_USER_VIEW_USER) { $viewasuser = true; } else { $viewasuser = false; } - if (is_null($userid)) { - $report = new reportbase($courseid, $gpr, $context, $USER->id); + $gui = new graded_users_iterator($course, null, $currentgroup); + $gui->require_active_enrolment($showonlyactiveenrol); + $gui->init(); + + if (is_null($userid)) { // Zero state. + $report = new gradereport_user\report\user($courseid, $gpr, $context, $USER->id, $viewasuser); if (isset($report)) { // Trigger report viewed event. $report->viewed(); } + $actionbar = new \gradereport_user\output\action_bar($context, $userview, null, $currentgroup); // Print header. - print_grade_page_head($course->id, 'report', 'user', ' ', false); + print_grade_page_head($courseid, 'report', 'user', ' ', false, null, true, + null, null, null, $actionbar); echo $report->output_report_zerostate(); - } else if (empty($userid)) { - $gui = new graded_users_iterator($course, null, $currentgroup); - $gui->require_active_enrolment($showonlyactiveenrol); - $gui->init(); - // Add tabs. - print_grade_page_head($courseid, 'report', 'user'); - groups_print_course_menu($course, $gpr->get_return_url('index.php?id=' . $courseid, ['userid' => 0])); - - if ($user_selector) { - echo $renderer->graded_users_selector('user', $course, $userid, $currentgroup, true); - } - - echo $renderer->view_user_selector($userid, $userview); + } else if ($userid == 0) { // Show all reports. + $actionbar = new \gradereport_user\output\action_bar($context, $userview, 0, $currentgroup); + print_grade_page_head($courseid, 'report', 'user', ' ', false, null, true, + null, null, null, $actionbar); while ($userdata = $gui->next_user()) { $user = $userdata->user; $report = new gradereport_user\report\user($courseid, $gpr, $context, $user->id, $viewasuser); + $userheading = $gradesrenderer->user_heading($report->user, $courseid, false); - $studentnamelink = html_writer::link( - new moodle_url( - '/user/view.php', - ['id' => $report->user->id, 'course' => $courseid] - ), - fullname($report->user) - ); - echo $OUTPUT->heading($studentnamelink); + echo $OUTPUT->heading($userheading); if ($report->fill_table()) { - echo '
' . $report->print_table(true); + echo $report->print_table(true); } - echo "

"; } $gui->close(); - } else { - // Only show one user's report. + } else { // Show one user's report. $report = new gradereport_user\report\user($courseid, $gpr, $context, $userid, $viewasuser); + $actionbar = new \gradereport_user\output\action_bar($context, $userview, $report->user->id, $currentgroup); - $studentnamelink = html_writer::link( - new moodle_url( - '/user/view.php', - ['id' => $report->user->id, 'course' => $courseid] - ), - fullname($report->user) - ); print_grade_page_head($courseid, 'report', 'user', - get_string('pluginname', 'gradereport_user') . ' - ' . $studentnamelink, - false, false, true, null, null, $report->user); - - groups_print_course_menu($course, $gpr->get_return_url('index.php?id=' . $courseid, ['userid' => 0])); - - if ($user_selector) { - $showallusersoptions = true; - echo $renderer->graded_users_selector('user', $course, $userid, $currentgroup, $showallusersoptions); - } - - echo $renderer->view_user_selector($userid, $userview); + $gradesrenderer->user_heading($report->user, $courseid), + false, false, true, null, null, null, $actionbar); if ($currentgroup && !groups_is_member($currentgroup, $userid)) { echo $OUTPUT->notification(get_string('groupusernotmember', 'error')); } else { if ($report->fill_table()) { - echo '
' . $report->print_table(true); + echo $report->print_table(true); } } + $userreportrenderer = $PAGE->get_renderer('gradereport_user'); + // Add previous/next user navigation. + echo $userreportrenderer->user_navigation($gui, $userid, $courseid); } } else { // Students will see just their own report. // Create a report instance. $report = new gradereport_user\report\user($courseid, $gpr, $context, $userid ?? $USER->id); + $userheading = $gradesrenderer->user_heading($report->user, $courseid, false); // Print the page. - print_grade_page_head($courseid, 'report', 'user', - get_string('pluginname', 'gradereport_user') . ' - ' . fullname($report->user)); + print_grade_page_head($courseid, 'report', 'user', ' '); + + echo $OUTPUT->heading($userheading); if ($report->fill_table()) { - echo '
' . $report->print_table(true); + echo $report->print_table(true); } } diff --git a/grade/report/user/lang/en/gradereport_user.php b/grade/report/user/lang/en/gradereport_user.php index ea69fdd55d1..b416f71050c 100644 --- a/grade/report/user/lang/en/gradereport_user.php +++ b/grade/report/user/lang/en/gradereport_user.php @@ -26,6 +26,8 @@ defined('MOODLE_INTERNAL') || die(); $string['allusersnum'] = 'All users ({$a})'; $string['eventgradereportviewed'] = 'Grade user report viewed'; +$string['gotonextreport'] = 'Go to next user report'; +$string['gotopreviousreport'] = 'Go to previous user report'; $string['pluginname'] = 'User report'; $string['user:view'] = 'View your own grade report'; $string['privacy:metadata:preference:gradereport_user_view_user'] = 'Whether to view report as current user or another user in the gradebook reports'; diff --git a/grade/report/user/pix/zero_state.svg b/grade/report/user/pix/zero_state.svg new file mode 100644 index 00000000000..26491db8084 --- /dev/null +++ b/grade/report/user/pix/zero_state.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/grade/report/user/renderer.php b/grade/report/user/renderer.php index f5b3a86fbea..cb009288639 100644 --- a/grade/report/user/renderer.php +++ b/grade/report/user/renderer.php @@ -80,4 +80,124 @@ class gradereport_user_renderer extends plugin_renderer_base { return $output; } + /** + * Renders the user selector trigger element. + * + * @param object $course The course object. + * @param int|null $userid The user ID. + * @param int|null $groupid The group ID. + * @return string The raw HTML to render. + * @throws coding_exception + */ + public function users_selector(object $course, ?int $userid = null, ?int $groupid = null): string { + + $data = [ + 'courseid' => $course->id, + 'groupid' => $groupid ?? 0, + ]; + + // If a particular option is selected (not in zero state). + if (!is_null($userid)) { + if ($userid) { // A single user selected. + $user = core_user::get_user($userid); + $data['selectedoption'] = [ + 'image' => $this->user_picture($user, ['size' => 40, 'link' => false]), + 'text' => fullname($user), + 'additionaltext' => $user->email, + ]; + } else { // All users selected. + // Get the total number of users. + $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); + $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); + $showonlyactiveenrol = $showonlyactiveenrol || + !has_capability('moodle/course:viewsuspendedusers', context_course::instance($course->id)); + $gui = new graded_users_iterator($course, null, $groupid); + $gui->require_active_enrolment($showonlyactiveenrol); + $gui->init(); + $totalusersnum = 0; + while ($userdata = $gui->next_user()) { + $totalusersnum++; + } + $gui->close(); + + $data['selectedoption'] = [ + 'text' => get_string('allusersnum', 'gradereport_user', $totalusersnum), + ]; + } + } + + $this->page->requires->js_call_amd('gradereport_user/user', 'init'); + return $this->render_from_template('gradereport_user/user_selector', $data); + } + + /** + * Creates and renders previous/next user navigation. + * + * @param graded_users_iterator $gui Objects that is used to iterate over a list of gradable users in the course. + * @param int $userid The ID of the current user. + * @param int $courseid The course ID. + * @return string The raw HTML to render. + */ + public function user_navigation(graded_users_iterator $gui, int $userid, int $courseid): string { + + $navigationdata = []; + + while ($userdata = $gui->next_user()) { + $users[$userdata->user->id] = $userdata->user; + } + $gui->close(); + + $arraykeys = array_keys($users); + $keynumber = array_search($userid, $arraykeys); + + // If the current user is not the first one in the list, find and render the previous user. + if ($keynumber !== 0) { + $previoususer = $users[$arraykeys[$keynumber - 1]]; + $navigationdata['previoususer'] = [ + 'name' => fullname($previoususer), + 'url' => (new moodle_url('/grade/report/user/index.php', ['id' => $courseid, 'userid' => $previoususer->id])) + ->out(false) + ]; + } + // If the current user is not the last one in the list, find and render the last user. + if ($keynumber < count($users) - 1) { + $nextuser = $users[$arraykeys[$keynumber + 1]]; + $navigationdata['nextuser'] = [ + 'name' => fullname($nextuser), + 'url' => (new moodle_url('/grade/report/user/index.php', ['id' => $courseid, 'userid' => $nextuser->id])) + ->out(false) + ]; + } + + return $this->render_from_template('gradereport_user/user_navigation', $navigationdata); + } + + /** + * Creates and renders 'view report as' selector element. + * + * @param int $userid The selected userid + * @param int $userview The current view user setting constant + * @param int $courseid The course ID. + * @return string The raw HTML to render. + */ + public function view_mode_selector(int $userid, int $userview, int $courseid): string { + + $viewasotheruser = new moodle_url('/grade/report/user/index.php', ['id' => $courseid, 'userid' => $userid, + 'userview' => GRADE_REPORT_USER_VIEW_USER]); + $viewasmyself = new moodle_url('/grade/report/user/index.php', ['id' => $courseid, 'userid' => $userid, + 'userview' => GRADE_REPORT_USER_VIEW_SELF]); + + $selectoroptions = [ + $viewasotheruser->out(false) => get_string('otheruser', 'core_grades'), + $viewasmyself->out(false) => get_string('myself', 'core_grades') + ]; + + $selectoractiveurl = $userview === GRADE_REPORT_USER_VIEW_USER ? $viewasotheruser : $viewasmyself; + + $viewasselect = new \core\output\select_menu('viewas', $selectoroptions, $selectoractiveurl->out(false)); + $viewasselect->set_label(get_string('viewas', 'core_grades')); + + return $this->render_from_template('gradereport_user/view_mode_selector', + $viewasselect->export_for_template($this)); + } } diff --git a/grade/report/user/styles.css b/grade/report/user/styles.css index 7b94533d796..daf75698b07 100644 --- a/grade/report/user/styles.css +++ b/grade/report/user/styles.css @@ -21,15 +21,16 @@ .path-grade-report-user .user-grade thead th { vertical-align: bottom; -} - -.path-grade-report-user .user-grade th { + border: none; text-align: left; + background-color: #f8f9fa; } .path-grade-report-user .user-grade td { min-width: 4.5em; - vertical-align: top; + background-color: #f8f9fa; + border: none; + vertical-align: middle; } .path-grade-report-user .user-grade .b1l { @@ -39,22 +40,125 @@ } .path-grade-report-user .user-grade tbody .column-itemname { - padding-left: 0; - padding-right: 8px; -} - -.path-grade-report-user .user-grade .column-itemname.item, -.path-grade-report-user .user-grade .gradeitemdescription { - font-weight: normal; padding-left: 24px; + padding-right: 8px; } .path-grade-report-user .user-grade .column-itemname.baggt, .path-grade-report-user .user-grade .column-itemname.baggb { - padding-left: 24px; + padding-left: 0; +} + +.path-grade-report-user .user-grade .column-itemname .itemicon, +.path-grade-report-user .user-grade .column-itemname .itemicon { + font-size: 18px; + width: 18px; + height: 18px; } .path-grade-report-user .user-grade .baggt, .path-grade-report-user .user-grade .baggb { font-weight: bold; } + +.path-grade-report-user .user-report-container { + margin: 20px 0 30px 0; + padding: 10px 10px; + background-color: #f8f9fa; +} + +.path-grade-report-user .user-grade tbody tr { + background-color: #f8f9fa; +} + +.path-grade-report-user .user-grade tbody tr:hover { + color: inherit; + background-color: #f8f9fa; +} + +.path-grade-report-user .user-grade tbody tr.spacer { + height: 0.5rem; +} + +.path-grade-report-user .user-grade td.item { + background-color: white; + border-top: 1px solid #dee2e6; + border-bottom: 1px solid #dee2e6; +} + +.path-grade-report-user .user-grade td.category { + background-color: white; + border: 1px solid #dee2e6; + padding-left: 10px; + font-weight: bold; +} + +.path-grade-report-user .user-grade td.category a[aria-expanded="true"] .expanded, +.path-grade-report-user .user-grade td.category a[aria-expanded="false"] .collapsed { + display: none; +} + +.path-grade-report-user .user-grade td.category a.toggle-category { + height: 24px; + width: 24px; + font-size: 12px; + line-height: 24px; + margin-right: 3px; +} + +.path-grade-report-user .user-grade td.category a.toggle-category i { + font-size: 12px; + width: 12px; + height: 12px; + color: #1d2125; + margin: 0; +} + +.path-grade-report-user .user-grade .column-itemname .small { + font-size: 70%; +} + +.path-grade-report-user .user-grade tr[data-hidden="true"] { + display: none; +} + +.path-grade-report-user .user-grade .category div { + min-height: 30px; + align-items: center; +} + +.path-grade-report-user .user-selector .userinitials { + width: 40px; + height: 40px; +} + +.path-grade-report-user .user-navigation .container { + max-width: 767px; +} + +.path-grade-report-user .view-user-selector label { + display: block; + font-size: 80%; + margin: 0; +} + +.path-grade-report-user .view-user-selector .dropdown-toggle { + padding: 0; + font-weight: bold; +} + +.path-grade-report-user .zero-state img { + width: 185px; +} + +@media print { + .path-grade-report-user .user-grade td.category { + border-left: none; + border-right: none; + } + + .path-grade-report-user .user-grade td.category a.toggle-category, + .path-grade-report-user #page-footer { + display: none; + } +} diff --git a/grade/report/user/templates/action_bar.mustache b/grade/report/user/templates/action_bar.mustache new file mode 100644 index 00000000000..0ddb3ac1be3 --- /dev/null +++ b/grade/report/user/templates/action_bar.mustache @@ -0,0 +1,100 @@ +{{! + This file is part of Moodle - http://moodle.org/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template gradereport_user/action_bar + + Actions bar for the grade user report. + + Context variables required for this template: + * generalnavselector - The data object containing the required properties to render the general navigation selector. + * groupselector - (optional) HTML that outputs the group selector + * userselector - (optional) HTML that outputs the user selector + * viewasselector - (optional) HTML that outputs the 'view report as' selector + + Example context (json): + { + "generalnavselector": { + "name": "Gradebook tertiary navigation selector", + "value": "opt2", + "baseid": "select-menu56789", + "selectedoption": "Gradebook setup", + "options": [ + { + "selected": false, + "isgroup": { + "name": "View", + "id": "select-menu-group1", + "options": [ + { + "name": "Grader report", + "value": "opt1", + "id": "select-menu-option1", + "selected": false + } + ] + } + }, + { + "selected": false, + "isgroup": { + "name": "Setup", + "id": "select-menu-group2", + "options": [ + { + "name": "Gradebook setup", + "value": "opt2", + "id": "select-menu-option2", + "selected": true + } + ] + } + } + ] + }, + "groupselector": "
", + "userselector": "
", + "viewasselector": "
" + } +}} +
+
+ {{#generalnavselector}} + + + {{/generalnavselector}} + {{#groupselector}} + + + {{/groupselector}} + {{#userselector}} + + + {{/userselector}} +
+ {{#viewasselector}} + + + + {{/viewasselector}} +
+
+
diff --git a/grade/report/user/templates/user_navigation.mustache b/grade/report/user/templates/user_navigation.mustache new file mode 100644 index 00000000000..5894e88decc --- /dev/null +++ b/grade/report/user/templates/user_navigation.mustache @@ -0,0 +1,61 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template gradereport_user/user_navigation + + The previous/next user navigation for the user report view. + + Context variables required for this template: + * previoususer - (optional) The object containing information about the previous user. + * name - The name of the previous user. + * url - The URL to the previous user report. + * nextuser - (optional) The object containing information about the next user. + * name - The name of the next user. + * url - The URL to the next user report. + + Example context (json): + { + "previoususer": { + "name": "John Smith", + "url": "https://example.com/grade/report/user/index.php?id=2&userid=3" + }, + "previoususer": { + "name": "Jane Doe", + "url": "https://example.com/grade/report/user/index.php?id=2&userid=5" + } + } +}} +
+
+ {{#previoususer}} + + {{/previoususer}} + {{#nextuser}} + + {{/nextuser}} +
+
diff --git a/grade/report/user/templates/user_report_category_content.mustache b/grade/report/user/templates/user_report_category_content.mustache new file mode 100644 index 00000000000..9694b2146b6 --- /dev/null +++ b/grade/report/user/templates/user_report_category_content.mustache @@ -0,0 +1,39 @@ +{{! + This file is part of Moodle - http://moodle.org/ + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template gradereport_user/user_report_category_content + + Template for the content of a category cell in the user report table. + + Context variables required for this template: + * categoryid - The grade category ID + * categoryname - The grade category name + + Example context (json): + { + "categoryid": "2", + "categoryname": "Grade category 1" + } +}} +
+ + + + + + + {{categoryname}} +
diff --git a/grade/report/user/templates/user_selector.mustache b/grade/report/user/templates/user_selector.mustache new file mode 100644 index 00000000000..985fa0c0b50 --- /dev/null +++ b/grade/report/user/templates/user_selector.mustache @@ -0,0 +1,74 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template gradereport_user/user_selector + + The user selector trigger element. + + Context variables required for this template: + * courseid - The course ID. + * groupid - The group ID. + * selectedoption - (optional) Object containing information about the selected option. + * image - The image corresponding to the selected option. + * text - The text of the selected option. + * additionaltext - (optional) Additional text displayed below the selected option (e.g. the user email) + + Example context (json): + { + "courseid": "2", + "groupid": "2", + "selectedoption": { + "image": "\"\"", + "text": "John Doe", + "additionaltext": "johndoe@example.com" + } + } +}} +
+ +
diff --git a/grade/report/user/templates/view_mode_selector.mustache b/grade/report/user/templates/view_mode_selector.mustache new file mode 100644 index 00000000000..cdad6813c94 --- /dev/null +++ b/grade/report/user/templates/view_mode_selector.mustache @@ -0,0 +1,62 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template gradereport_user/view_mode_selector + + The view mode selector. + + Context variables required for this template: + * name - name of the form element + * value - value of the form element + * baseid - id of the dropdown element and to be used to generate id for other elements used internally + * label - Element label + * labelattributes - Label attributes. + * selectedoption - Text of the selected option + * options - Array of options for the select with value, name, selected, isgroup and id properites. + + Example context (json): + { + "name": "menuname", + "value": "opt2", + "baseid": "select-menu56789", + "label": "View report as", + "labelattributes": [], + "selectedoption": "Myself", + "options": [ + { + "name": "User", + "value": "opt1", + "id": "select-menu-option1", + "selected": false + }, + { + "name": "Myself", + "value": "opt2", + "id": "select-menu-option2", + "selected": true + } + ] + } +}} +
+ {{>core/select_menu}} +
+{{#js}} + document.querySelector('#{{baseid}}').addEventListener('change', function(e) { + window.location.href = e.target.value; + }); +{{/js}} diff --git a/grade/report/user/templates/zero_state.mustache b/grade/report/user/templates/zero_state.mustache index 90789f8e4ee..69b9cb174e4 100644 --- a/grade/report/user/templates/zero_state.mustache +++ b/grade/report/user/templates/zero_state.mustache @@ -21,29 +21,11 @@ Example context (json): { - "imglink": "http://foo.bar/gradereport/?userid=25", - "courseid": "2", - "groupmodeenabled": true, - "groupactionbaseurl": "index.php", - "groupid": 2 + "imglink": "http://foo.bar/gradereport/?userid=25" } }} -
- Temp +
+ {{#str}}selectuser, gradereport_user{{/str}}

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

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

- {{#groupmodeenabled}} - {{#str}}selectgrouplink, grades{{/str}} - {{/groupmodeenabled}} - {{#str}}selectuserlink, grades{{/str}}
diff --git a/grade/report/user/tests/behat/toggle_grade_categories.feature b/grade/report/user/tests/behat/toggle_grade_categories.feature new file mode 100644 index 00000000000..9c24725e908 --- /dev/null +++ b/grade/report/user/tests/behat/toggle_grade_categories.feature @@ -0,0 +1,55 @@ +@core @core_grades @gradereport_user @javascript +Feature: User can toggle the visibility of the grade categories within the user grade report. + In order to focus only on the information that I am interested in + As a teacher + I need to be able to easily toggle the visibility of grade categories in the user grade report + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | t1 | + | student1 | Student | 1 | student1@example.com | s1 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And the following "grade categories" exist: + | fullname | course | + | Category 1 | C1 | + And the following "activities" exist: + | activity | course | idnumber | name | intro | grade | + | assign | C1 | a1 | Test assignment one | Submit something! | 300 | + And the following "activities" exist: + | activity | course | idnumber | name | intro | gradecategory | grade | gradepass | + | assign | C1 | a2 | Test assignment two | Submit something! | Category 1 | 100 | 50 | + And I log in as "teacher1" + And I am on "Course" course homepage + + Scenario: A teacher can search for and find a user to view + Given I navigate to "View > User report" in the course gradebook + And I click on "Student 1" in the "user" search widget + And I should see "Test assignment one" in the "user-grade" "table" + And I should see "Test assignment two" in the "user-grade" "table" + And I should see "Category 1 total" in the "user-grade" "table" + And I should see "Course total" in the "user-grade" "table" + # Hide the grade category 'Category 1'. + When I click on ".toggle-category" "css_element" in the "Category 1" "table_row" + Then I should not see "Test assignment two" in the "user-grade" "table" + And I should not see "Category 1 total" in the "user-grade" "table" + And I should see "Test assignment one" in the "user-grade" "table" + And I should see "Course total" in the "user-grade" "table" + # Show the grade category 'Category 1'. + And I click on ".toggle-category" "css_element" in the "Category 1" "table_row" + And I should see "Test assignment two" in the "user-grade" "table" + And I should see "Category 1 total" in the "user-grade" "table" + And I should see "Test assignment one" in the "user-grade" "table" + And I should see "Course total" in the "user-grade" "table" + # Hide the grade category 'Course'. + And I click on ".toggle-category" "css_element" in the "Course" "table_row" + And I should not see "Test assignment two" in the "user-grade" "table" + And I should not see "Category 1 total" in the "user-grade" "table" + And I should not see "Test assignment one" in the "user-grade" "table" + And I should not see "Course total" in the "user-grade" "table" diff --git a/grade/report/user/tests/behat/user_report_navigation.feature b/grade/report/user/tests/behat/user_report_navigation.feature new file mode 100644 index 00000000000..53afc34fae9 --- /dev/null +++ b/grade/report/user/tests/behat/user_report_navigation.feature @@ -0,0 +1,65 @@ +@core @core_grades @gradereport_user @javascript +Feature: Teacher can navigate to the previous or next user report. + In order to get go the previous or next user report + As a teacher + I need to click on the previous/next navigation links + + Background: + Given the following "courses" exist: + | fullname | shortname | category | + | Course | C1 | 0 | + And the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | t1 | + | student1 | Student | 1 | student1@example.com | s1 | + | student2 | Student | 2 | student2@example.com | s2 | + | student3 | Student | 3 | student3@example.com | s3 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + | student3 | C1 | student | + And the following "activities" exist: + | activity | course | idnumber | name | intro | grade | + | assign | C1 | a1 | Test assignment one | Submit something! | 300 | + And I log in as "teacher1" + And I am on "Course" course homepage + + Scenario: A teacher can navigate to the next user report + Given I navigate to "View > User report" in the course gradebook + And I click on "Student 1" in the "user" search widget + And I should see "Student 1" in the ".user-heading" "css_element" + And ".previous" "css_element" should not exist in the ".user-navigation" "css_element" + And ".next" "css_element" should exist in the ".user-navigation" "css_element" + And I should see "Student 2" in the ".next" "css_element" + When I click on "Student 2" "link" in the ".next" "css_element" + Then I should see "Student 2" in the ".user-heading" "css_element" + And ".previous" "css_element" should exist in the ".user-navigation" "css_element" + And I should see "Student 1" in the ".previous" "css_element" + And ".next" "css_element" should exist in the ".user-navigation" "css_element" + And I should see "Student 3" in the ".next" "css_element" + And I click on "Student 3" "link" in the ".next" "css_element" + And I should see "Student 3" in the ".user-heading" "css_element" + And ".previous" "css_element" should exist in the ".user-navigation" "css_element" + And I should see "Student 2" in the ".previous" "css_element" + And ".next" "css_element" should not exist in the ".user-navigation" "css_element" + + Scenario: A teacher can navigate to the previous user report + Given I navigate to "View > User report" in the course gradebook + And I click on "Student 3" in the "user" search widget + And I should see "Student 3" in the ".user-heading" "css_element" + And ".previous" "css_element" should exist in the ".user-navigation" "css_element" + And I should see "Student 2" in the ".previous" "css_element" + And ".next" "css_element" should not exist in the ".user-navigation" "css_element" + When I click on "Student 2" "link" in the ".previous" "css_element" + Then I should see "Student 2" in the ".user-heading" "css_element" + And ".previous" "css_element" should exist in the ".user-navigation" "css_element" + And I should see "Student 1" in the ".previous" "css_element" + And ".next" "css_element" should exist in the ".user-navigation" "css_element" + And I should see "Student 3" in the ".next" "css_element" + And I click on "Student 1" "link" in the ".previous" "css_element" + And I should see "Student 1" in the ".user-heading" "css_element" + And ".previous" "css_element" should not exist in the ".user-navigation" "css_element" + And ".next" "css_element" should exist in the ".user-navigation" "css_element" + And I should see "Student 2" in the ".next" "css_element" diff --git a/grade/report/user/tests/behat/user_view.feature b/grade/report/user/tests/behat/user_view.feature index ded04bff316..941d37bfc59 100644 --- a/grade/report/user/tests/behat/user_view.feature +++ b/grade/report/user/tests/behat/user_view.feature @@ -50,7 +50,7 @@ Feature: View the user report as the student will see it Scenario: View the report as the teacher themselves When I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "user" search widget - And I select "Myself" from the "View report as" singleselect + And I set the field "View report as" to "Myself" Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 50.00 % | 80.00 | 0–100 | 80.00 % | 13.33 % | @@ -66,7 +66,7 @@ Feature: View the user report as the student will see it Scenario: View the report as the student from both the teachers and students perspective When I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "user" search widget - And I select "User" from the "View report as" singleselect + And I set the field "View report as" to "User" Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | - | 80.00 | 0–100 | 80.00 % | - | @@ -108,7 +108,7 @@ Feature: View the user report as the student will see it And I press "Save changes" And I navigate to "View > User report" in the course gradebook When I click on "Student 1" in the "user" search widget - And I select "User" from the "View report as" singleselect + And I set the field "View report as" to "User" Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 50.00 % | 80.00 | 0–100 | 80.00 % | 26.67 % | @@ -146,7 +146,7 @@ Feature: View the user report as the student will see it And I press "Save changes" And I navigate to "View > User report" in the course gradebook When I click on "Student 1" in the "user" search widget - And I select "User" from the "View report as" singleselect + And I set the field "View report as" to "User" Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 50.00 % | 80.00 | 0–100 | 80.00 % | 13.33 % | @@ -192,7 +192,7 @@ Feature: View the user report as the student will see it And I press "Save changes" And I navigate to "View > User report" in the course gradebook When I click on "Student 1" in the "user" search widget - And I select "User" from the "View report as" singleselect + And I set the field "View report as" to "User" Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 50.00 % | 80.00 | 0–100 | 80.00 % | 13.33 % | diff --git a/grade/report/user/tests/behat/usersearch.feature b/grade/report/user/tests/behat/usersearch.feature index da1d8b852e5..40d654abbe1 100644 --- a/grade/report/user/tests/behat/usersearch.feature +++ b/grade/report/user/tests/behat/usersearch.feature @@ -21,7 +21,7 @@ Feature: Within the User report, a teacher can search for users. Scenario: A teacher can search for and find a user to view Given I navigate to "View > User report" in the course gradebook - When I click on "Click to select a user" "link" + When I click on ".userwidget" "css_element" Then I confirm "Student 1" in "Select a user" search within the gradebook widget exists And I confirm "Student 2" in "Select a user" search within the gradebook widget exists And I confirm "Student 32" in "Select a user" search within the gradebook widget exists diff --git a/grade/report/user/tests/externallib_test.php b/grade/report/user/tests/externallib_test.php index 6edf27c058f..d574b6d5ea3 100644 --- a/grade/report/user/tests/externallib_test.php +++ b/grade/report/user/tests/externallib_test.php @@ -111,8 +111,9 @@ class externallib_test extends externallib_advanced_testcase { // Read returned grades. $studentreturnedgrades = []; + $studentreturnedgrades[$studentgrades['tables'][0]['userid']] = - (int) $studentgrades['tables'][0]['tabledata'][1]['grade']['content']; + (int) $studentgrades['tables'][0]['tabledata'][2]['grade']['content']; $this->assertEquals($s1grade, $studentreturnedgrades[$student1->id]); } @@ -139,7 +140,7 @@ class externallib_test extends externallib_advanced_testcase { $this->assertTrue(count($studentgrade['warnings']) == 0); $this->assertTrue(count($studentgrade['tables']) == 1); - $student1returnedgrade = (int) $studentgrade['tables'][0]['tabledata'][1]['grade']['content']; + $student1returnedgrade = (int) $studentgrade['tables'][0]['tabledata'][2]['grade']['content']; $this->assertEquals($s1grade, $student1returnedgrade); } diff --git a/grade/templates/group_selector.mustache b/grade/templates/group_selector.mustache new file mode 100644 index 00000000000..7adef07653d --- /dev/null +++ b/grade/templates/group_selector.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_grades/group_selector + + The group selector trigger element. + + Context variables required for this template: + * label - The label text fot the group selector element. + * courseid - The course ID. + * groupactionbaseurl - The base URL for the group action. + * selectedgroup - The text of the selected group option. + + Example context (json): + { + "label": "Select separate groups", + "courseid": "2", + "groupactionbaseurl": "index.php?item=test", + "selectedgroup": "Group 1" + } +}} +
+ +
diff --git a/grade/templates/user_heading.mustache b/grade/templates/user_heading.mustache new file mode 100644 index 00000000000..9455444bb87 --- /dev/null +++ b/grade/templates/user_heading.mustache @@ -0,0 +1,73 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template core_grades/user_heading + + Custom user heading. + + Context variables required for this template: + * image - HTML img element representing the avatar of the user. + * userprofileurl - The URL to the user profile page. + * name - The name of the user + * buttons - List of buttons to be displayed in the heading for the user report (e.g. 'message', 'add user to contacts') + + Example context (json): + { + "image": "\"\"", + "userprofileurl": "http://example.com/user/view.php?id=6&course=8", + "name": "John Doe", + "buttons": [ + { + "linkattributes": [ + { + "name": "role", + "value": "button" + }, + { + "name": "data-userid", + "value": "3" + } + ], + "title": "Message", + "url": "http://example.com/message/index.php?user1=2&user2=3&addcontact=3", + "icon": { + "name": "t/message", + "component": "core" + } + } + ] + } +}} + diff --git a/grade/tests/behat/grade_aggregation.feature b/grade/tests/behat/grade_aggregation.feature index ca396878bed..c7508f08930 100644 --- a/grade/tests/behat/grade_aggregation.feature +++ b/grade/tests/behat/grade_aggregation.feature @@ -351,7 +351,7 @@ Feature: We can use calculated grade totals And I press "Save changes" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "user" search widget - And I select "Myself" from the "View report as" singleselect + And I set the field "View report as" to "Myself" And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Contribution to course total | | Test assignment five | 28.57 % | 10.00 (50.00 %) | 0–20 | 1.03 % | @@ -543,7 +543,7 @@ Feature: We can use calculated grade totals Then I should see "75.00 (16.85 %)" in the ".course" "css_element" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "user" search widget - And I select "Myself" from the "View report as" singleselect + And I set the field "View report as" to "Myself" And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Test assignment five | 57.14 % | 10.00 (50.00 %) | 2.25 % | diff --git a/grade/tests/behat/grade_calculated_grade_items.feature b/grade/tests/behat/grade_calculated_grade_items.feature index da234625a12..79136ec40de 100644 --- a/grade/tests/behat/grade_calculated_grade_items.feature +++ b/grade/tests/behat/grade_calculated_grade_items.feature @@ -88,7 +88,7 @@ Feature: Calculated grade items can be used in the gradebook | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | | Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 0–40 | 93.75 % | - | | Course total | - | 37.50 | 0–40 | 93.75 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 65.00 | 0–100 | 65.00 % | - | @@ -105,7 +105,7 @@ Feature: Calculated grade items can be used in the gradebook | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | | Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 0–40 | 93.75 % | - | | Course total | - | 37.50 | 0–40 | 93.75 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 65.00 | 0–100 | 65.00 % | - | @@ -155,7 +155,7 @@ Feature: Calculated grade items can be used in the gradebook | grade item 1 | 71.43 % | 75.00 | 0–100 | 75.00 % | 53.57 % | | calc item | 28.57 % | 37.50 | 0–40 | 93.75 % | 26.79 % | | Course total | - | 112.50 | 0–140 | 80.36 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | 71.43 % | 65.00 | 0–100 | 65.00 % | 46.43 % | diff --git a/grade/tests/behat/grade_calculated_grade_items_20150627.feature b/grade/tests/behat/grade_calculated_grade_items_20150627.feature index 5fcd2e103d5..6437d519458 100644 --- a/grade/tests/behat/grade_calculated_grade_items_20150627.feature +++ b/grade/tests/behat/grade_calculated_grade_items_20150627.feature @@ -89,7 +89,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | | Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 0–100 | 37.50 % | - | | Course total | - | 37.50 | 0–100 | 37.50 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 65.00 | 0–100 | 65.00 % | - | @@ -106,7 +106,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 | grade item 1 | - | 75.00 | 0–100 | 75.00 % | - | | Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 0–100 | 37.50 % | - | | Course total | - | 37.50 | 0–100 | 37.50 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | - | 65.00 | 0–100 | 65.00 % | - | @@ -156,7 +156,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506 | grade item 1 | 50.00 % | 75.00 | 0–100 | 75.00 % | 37.50 % | | calc item | 50.00 % | 37.50 | 0–100 | 37.50 % | 18.75 % | | Course total | - | 112.50 | 0–200 | 56.25 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | grade item 1 | 50.00 % | 65.00 | 0–100 | 65.00 % | 32.50 % | diff --git a/grade/tests/behat/grade_grade_minmax_change.feature b/grade/tests/behat/grade_grade_minmax_change.feature index e6b3fd58e50..61ec6baad59 100644 --- a/grade/tests/behat/grade_grade_minmax_change.feature +++ b/grade/tests/behat/grade_grade_minmax_change.feature @@ -52,7 +52,7 @@ Feature: We can change the maximum and minimum number of points for manual items Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | 100.00 % | 10.00 | 100.00 % | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | 100.00 % | 8.00 | 80.00 % | @@ -68,7 +68,7 @@ Feature: We can change the maximum and minimum number of points for manual items And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | 100.00 % | 20.00 | 100.00 % | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | 100.00 % | 16.00 | 80.00 % | diff --git a/grade/tests/behat/grade_hidden_items.feature b/grade/tests/behat/grade_hidden_items.feature index 186cbb6c0e2..35aa047654a 100644 --- a/grade/tests/behat/grade_hidden_items.feature +++ b/grade/tests/behat/grade_hidden_items.feature @@ -56,7 +56,7 @@ Feature: Student and teacher's view of aggregated grade items is consistent when And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "user" search widget - And I select "Myself" from the "View report as" singleselect + And I set the field "View report as" to "Myself" Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test assignment one | 100.00 % | 50.00 | 0–100 | 50.00 % | 25.00 % | diff --git a/grade/tests/behat/grade_hidden_items_locked_category.feature b/grade/tests/behat/grade_hidden_items_locked_category.feature index 9a4b061c2bd..264c7378880 100644 --- a/grade/tests/behat/grade_hidden_items_locked_category.feature +++ b/grade/tests/behat/grade_hidden_items_locked_category.feature @@ -48,7 +48,7 @@ Feature: Hidden grade items should be hidden when grade category is locked, but And I am on "Course 1" course homepage And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "user" search widget - And I select "Myself" from the "View report as" singleselect + When I set the field "View report as" to "Myself" Then the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | Test locked category total | 100.00 % | 50.00 | 0–100 | 50.00 % | - | diff --git a/grade/tests/behat/grade_mingrade.feature b/grade/tests/behat/grade_mingrade.feature index 5f3fd1d8330..5fdd5f4038a 100644 --- a/grade/tests/behat/grade_mingrade.feature +++ b/grade/tests/behat/grade_mingrade.feature @@ -109,7 +109,7 @@ Feature: We can use a minimum grade different than zero | Manual item 4 | 66.67 % | -10.00 | -1.82 % | | Manual item 5 | 50.00 % | 50.00 | 9.09 % | | Manual item 6 | 50.00 % | 75.00 | 13.64 % | - And I set the field "Select all or one user" to "Student 2" + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Contribution to course total | | Manual item 1 | 18.18 % | 0.00 | 0.00 % | diff --git a/grade/tests/behat/grade_minmax.feature b/grade/tests/behat/grade_minmax.feature index 36a520936d9..6d3be2f3e49 100644 --- a/grade/tests/behat/grade_minmax.feature +++ b/grade/tests/behat/grade_minmax.feature @@ -86,7 +86,7 @@ Feature: We can choose what min or max grade to use when aggregating grades. | MI 5 | 20.00 % | 100.00 | 0–100 | 100.00 % | 20.00 % | | CAT1 total | 40.00 % | 150.00 | 0–200 | 75.00 % | - | | Course total | - | 350.00 | 0–500 | 70.00 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 1 | 33.33 % | 20.00 | 0–100 | 20.00 % | 6.67 % | @@ -116,7 +116,7 @@ Feature: We can choose what min or max grade to use when aggregating grades. | MI 5 | 25.00 % | 100.00 | 0–100 | 100.00 % | 25.00 % | | CAT1 total | 37.50 % | 150.00 | 0–150 | 100.00 % | - | | Course total | - | 350.00 | 0–400 | 87.50 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 1 | 25.00 % | 20.00 | 5–50 | 33.33 % | 10.00 % | @@ -136,7 +136,7 @@ Feature: We can choose what min or max grade to use when aggregating grades. | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 5 | 40.00 % | 150.00 | 0–200 | 75.00 % | 30.00 % | | Course total | - | 400.00 | 0–500 | 80.00 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 5 | 66.67 % | 30.00 | 0–200 | 15.00 % | 10.00 % | @@ -155,7 +155,7 @@ Feature: We can choose what min or max grade to use when aggregating grades. | MI 5 | 33.33 % | 150.00 | 0–200 | 75.00 % | 25.00 % | | CAT1 total | 33.33 % | 150.00 | 0–200 | 75.00 % | - | | Course total | - | 400.00 | 0–600 | 66.67 % | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Calculated weight | Grade | Range | Percentage | Contribution to course total | | MI 1 | 25.00 % | 20.00 | 0–100 | 20.00 % | 5.00 % | diff --git a/grade/tests/behat/grade_single_item_scales.feature b/grade/tests/behat/grade_single_item_scales.feature index 9cd97bf9c52..e1d0112fc7b 100644 --- a/grade/tests/behat/grade_single_item_scales.feature +++ b/grade/tests/behat/grade_single_item_scales.feature @@ -72,7 +72,7 @@ Feature: View gradebook when single item scales are used | Test assignment one | Ace! | Ace!–Ace! | 100.00 % | | EN Sub category 1 total| 1.00 | 0–1 | - | | Course total | 1.00 | 0–1 | - | - And I select "Student 2" from the "Select all or one user" singleselect + And I click on "Student 2" in the "user" search widget And the following should exist in the "user-grade" table: | Grade item | Grade | Range | Contribution to course total | | Test assignment one | - | Ace!–Ace! | - | diff --git a/grade/tests/reportuserlib_test.php b/grade/tests/reportuserlib_test.php index 7865589770f..19f270065a8 100644 --- a/grade/tests/reportuserlib_test.php +++ b/grade/tests/reportuserlib_test.php @@ -133,9 +133,9 @@ class reportuserlib_test extends \advanced_testcase { $forum1->set_parent($gradecategory->id); $report = $this->create_report($course, $student, $coursecontext); - // Lead column + course + (category + category grade item) + (2 x activity) = 6 - $this->assertEquals(6, $report->inject_rowspans($report->gtree->top_element)); - $this->assertEquals(6, $report->gtree->top_element['rowspan']); + // Lead column + course + (category + lead column + category grade item) + (2 x activity) = 7. + $this->assertEquals(7, $report->inject_rowspans($report->gtree->top_element)); + $this->assertEquals(7, $report->gtree->top_element['rowspan']); // Lead column -> the category -> the forum activity = 3 $this->assertEquals(3, $report->maxdepth); @@ -165,9 +165,9 @@ class reportuserlib_test extends \advanced_testcase { accesslib_clear_all_caches_for_unit_testing(); $report = $this->create_report($course, $user, $coursecontext); - // Lead column + course + (category + category grade item) + (2 x activity) = 6 - $this->assertEquals(6, $report->inject_rowspans($report->gtree->top_element), $message); - $this->assertEquals(6, $report->gtree->top_element['rowspan'], $message); + // Lead column + course + (category + lead column + category grade item) + (2 x activity) = 7. + $this->assertEquals(7, $report->inject_rowspans($report->gtree->top_element), $message); + $this->assertEquals(7, $report->gtree->top_element['rowspan'], $message); // Lead column -> the category -> the forum activity = 3 $this->assertEquals(3, $report->maxdepth, $message); } diff --git a/lang/en/moodle.php b/lang/en/moodle.php index c36f447ff46..c436ec1d28e 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1939,6 +1939,8 @@ $string['selectedcategories'] = 'Selected categories'; $string['selectednowmove'] = '{$a} files selected for moving. Now go into the destination folder and press \'Move files to here\''; $string['selectfiles'] = 'Select files'; $string['selectfiltertype'] = 'Select'; +$string['selectgroupsseparate'] = 'Select separate groups'; +$string['selectgroupsvisible'] = 'Select visible groups'; $string['selectitem'] = 'Select \'{$a}\''; $string['selectmoduletoviewhelp'] = 'Select an activity or resource to view its help. diff --git a/message/amd/build/toggle_contact_button.min.js b/message/amd/build/toggle_contact_button.min.js index c07297dd275..d4e3aa74795 100644 --- a/message/amd/build/toggle_contact_button.min.js +++ b/message/amd/build/toggle_contact_button.min.js @@ -5,6 +5,6 @@ * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -define("core_message/toggle_contact_button",["jquery","core/ajax","core/templates","core/notification","core/custom_interaction_events"],(function($,Ajax,Templates,Notification,CustomEvents){var getUserId=function(element){return element.attr("data-userid")},getCurrentUserId=function(element){return element.attr("data-currentuserid")},isLoading=function(element){return element.hasClass("loading")||element.attr("disabled")},sendRequest=function(element,request){return isLoading(element)?$.Deferred():(element.addClass("loading"),element.attr("disabled","disabled"),Ajax.call([request])[0].fail(Notification.exception).always((function(){element.removeClass("loading"),element.removeAttr("disabled")})))};return{enhance:function(element){(element=$(element)).children(".loading-icon").length||Templates.render("core/loading",{}).done((function(html,js){element.append(html,js)})),CustomEvents.define(element,[CustomEvents.events.activate]),element.on(CustomEvents.events.activate,(function(e,data){!function(element){return"1"==element.attr("data-is-contact")}(element)?function(element){if(!isLoading(element)){var request={methodname:"core_message_create_contact_request",args:{userid:getCurrentUserId(element),requesteduserid:getUserId(element)}};sendRequest(element,request).done((function(){!function(element){element.attr("data-is-contact","1")}(element),Templates.render("message/remove_contact_button",{}).done((function(html,js){Templates.replaceNodeContents(element,html,js)}))}))}}(element):function(element){if(!isLoading(element)){var request={methodname:"core_message_delete_contacts",args:{userids:[getUserId(element)]}};sendRequest(element,request).done((function(){!function(element){element.attr("data-is-contact","0")}(element),Templates.render("message/add_contact_button",{}).done((function(html,js){Templates.replaceNodeContents(element,html,js)}))}))}}(element),e.preventDefault(),data.originalEvent.preventDefault()}))}}})); +define("core_message/toggle_contact_button",["jquery","core/ajax","core/templates","core/notification","core/custom_interaction_events"],(function($,Ajax,Templates,Notification,CustomEvents){var getUserId=function(element){return element.attr("data-userid")},getCurrentUserId=function(element){return element.attr("data-currentuserid")},displayTextLabel=function(element){return"1"==element.attr("data-display-text-label")},isLoading=function(element){return element.hasClass("loading")||element.attr("disabled")},sendRequest=function(element,request){return isLoading(element)?$.Deferred():(element.addClass("loading"),element.attr("disabled","disabled"),Ajax.call([request])[0].fail(Notification.exception).always((function(){element.removeClass("loading"),element.removeAttr("disabled")})))};return{enhance:function(element){(element=$(element)).children(".loading-icon").length||Templates.render("core/loading",{}).done((function(html,js){element.append(html,js)})),CustomEvents.define(element,[CustomEvents.events.activate]),element.on(CustomEvents.events.activate,(function(e,data){!function(element){return"1"==element.attr("data-is-contact")}(element)?function(element){if(!isLoading(element)){var request={methodname:"core_message_create_contact_request",args:{userid:getCurrentUserId(element),requesteduserid:getUserId(element)}};sendRequest(element,request).done((function(){!function(element){element.attr("data-is-contact","1")}(element);const templateContext={displaytextlabel:displayTextLabel(element)};Templates.render("message/remove_contact_button",templateContext).done((function(html,js){Templates.replaceNodeContents(element,html,js)}))}))}}(element):function(element){if(!isLoading(element)){var request={methodname:"core_message_delete_contacts",args:{userids:[getUserId(element)]}};sendRequest(element,request).done((function(){!function(element){element.attr("data-is-contact","0")}(element);const templateContext={displaytextlabel:displayTextLabel(element)};Templates.render("message/add_contact_button",templateContext).done((function(html,js){Templates.replaceNodeContents(element,html,js)}))}))}}(element),e.preventDefault(),data.originalEvent.preventDefault()}))}}})); //# sourceMappingURL=toggle_contact_button.min.js.map \ No newline at end of file diff --git a/message/amd/build/toggle_contact_button.min.js.map b/message/amd/build/toggle_contact_button.min.js.map index bf7a37f9b8d..2760ca1ea6d 100644 --- a/message/amd/build/toggle_contact_button.min.js.map +++ b/message/amd/build/toggle_contact_button.min.js.map @@ -1 +1 @@ -{"version":3,"file":"toggle_contact_button.min.js","sources":["../src/toggle_contact_button.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to add/remove contact using ajax.\n *\n * @module core_message/toggle_contact_button\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/custom_interaction_events'],\n function($, Ajax, Templates, Notification, CustomEvents) {\n\n /**\n * Check the state of the element, if the current user is a contact or not.\n *\n * @method isContact\n * @param {object} element jQuery object for the button\n * @return {bool}\n */\n var isContact = function(element) {\n return element.attr('data-is-contact') == '1';\n };\n\n /**\n * Record that the user is a contact.\n *\n * @method setContact\n * @param {object} element jQuery object for the button\n */\n var setContact = function(element) {\n element.attr('data-is-contact', '1');\n };\n\n /**\n * Record that the user is not a contact.\n *\n * @method setNotContact\n * @param {object} element jQuery object for the button\n */\n var setNotContact = function(element) {\n element.attr('data-is-contact', '0');\n };\n\n /**\n * Get the id for the user being viewed.\n *\n * @method getUserId\n * @param {object} element jQuery object for the button\n * @return {int}\n */\n var getUserId = function(element) {\n return element.attr('data-userid');\n };\n\n /**\n * Get the id for the logged in user.\n *\n * @method getUserId\n * @param {object} element jQuery object for the button\n * @return {int}\n */\n var getCurrentUserId = function(element) {\n return element.attr('data-currentuserid');\n };\n\n /**\n * Check if this element is currently loading.\n *\n * @method isLoading\n * @param {object} element jQuery object for the button\n * @return {bool}\n */\n var isLoading = function(element) {\n return element.hasClass('loading') || element.attr('disabled');\n };\n\n /**\n * Sends an ajax request to the server and handles the element state\n * while the request is being performed.\n *\n * @method sendRequest\n * @param {object} element jQuery object for the button\n * @param {object} request Request hash to send\n * @return {object} jQuery promise\n */\n var sendRequest = function(element, request) {\n if (isLoading(element)) {\n return $.Deferred();\n }\n\n element.addClass('loading');\n element.attr('disabled', 'disabled');\n\n return Ajax.call([request])[0]\n .fail(Notification.exception)\n .always(function() {\n element.removeClass('loading');\n element.removeAttr('disabled');\n });\n };\n\n /**\n * Send a request to the server to add the current user as\n * a contact. The contents of the button are changed to the\n * remove contact button upon success.\n *\n * @method addContact\n * @param {object} element jQuery object for the button\n */\n var addContact = function(element) {\n if (isLoading(element)) {\n return;\n }\n\n var request = {\n methodname: 'core_message_create_contact_request',\n args: {\n userid: getCurrentUserId(element),\n requesteduserid: getUserId(element),\n }\n };\n sendRequest(element, request).done(function() {\n setContact(element);\n Templates.render('message/remove_contact_button', {}).done(function(html, js) {\n Templates.replaceNodeContents(element, html, js);\n });\n });\n };\n\n /**\n * Send a request to the server to remove the current user as\n * a contact. The contents of the button are changed to the\n * add contact button upon success.\n *\n * @method removeContact\n * @param {object} element jQuery object for the button\n */\n var removeContact = function(element) {\n if (isLoading(element)) {\n return;\n }\n\n var request = {\n methodname: 'core_message_delete_contacts',\n args: {\n userids: [getUserId(element)],\n }\n };\n\n sendRequest(element, request).done(function() {\n setNotContact(element);\n Templates.render('message/add_contact_button', {}).done(function(html, js) {\n Templates.replaceNodeContents(element, html, js);\n });\n });\n };\n\n /**\n * Enhances the given element with a loading gif and event handles to make\n * ajax requests to add or remove a contact where appropriate.\n *\n * @public\n * @method enhance\n * @param {object} element jQuery object for the button\n */\n var enhance = function(element) {\n element = $(element);\n\n if (!element.children('.loading-icon').length) {\n // Add the loading gif if it isn't already there.\n Templates.render('core/loading', {}).done(function(html, js) {\n element.append(html, js);\n });\n }\n\n CustomEvents.define(element, [CustomEvents.events.activate]);\n\n element.on(CustomEvents.events.activate, function(e, data) {\n if (isContact(element)) {\n removeContact(element);\n } else {\n addContact(element);\n }\n e.preventDefault();\n data.originalEvent.preventDefault();\n });\n };\n\n return {\n enhance: enhance\n };\n});\n"],"names":["define","$","Ajax","Templates","Notification","CustomEvents","getUserId","element","attr","getCurrentUserId","isLoading","hasClass","sendRequest","request","Deferred","addClass","call","fail","exception","always","removeClass","removeAttr","enhance","children","length","render","done","html","js","append","events","activate","on","e","data","isContact","methodname","args","userid","requesteduserid","setContact","replaceNodeContents","addContact","userids","setNotContact","removeContact","preventDefault","originalEvent"],"mappings":";;;;;;;AAsBAA,4CAAO,CAAC,SAAU,YAAa,iBAAkB,oBAAqB,mCAC9D,SAASC,EAAGC,KAAMC,UAAWC,aAAcC,kBAwC3CC,UAAY,SAASC,gBACdA,QAAQC,KAAK,gBAUpBC,iBAAmB,SAASF,gBACrBA,QAAQC,KAAK,uBAUpBE,UAAY,SAASH,gBACdA,QAAQI,SAAS,YAAcJ,QAAQC,KAAK,aAYnDI,YAAc,SAASL,QAASM,gBAC5BH,UAAUH,SACHN,EAAEa,YAGbP,QAAQQ,SAAS,WACjBR,QAAQC,KAAK,WAAY,YAElBN,KAAKc,KAAK,CAACH,UAAU,GACvBI,KAAKb,aAAac,WAClBC,QAAO,WACJZ,QAAQa,YAAY,WACpBb,QAAQc,WAAW,uBA2FxB,CACHC,QAxBU,SAASf,UACnBA,QAAUN,EAAEM,UAECgB,SAAS,iBAAiBC,QAEnCrB,UAAUsB,OAAO,eAAgB,IAAIC,MAAK,SAASC,KAAMC,IACrDrB,QAAQsB,OAAOF,KAAMC,OAI7BvB,aAAaL,OAAOO,QAAS,CAACF,aAAayB,OAAOC,WAElDxB,QAAQyB,GAAG3B,aAAayB,OAAOC,UAAU,SAASE,EAAGC,OA9JzC,SAAS3B,eACqB,KAAnCA,QAAQC,KAAK,mBA8JZ2B,CAAU5B,SArEL,SAASA,aAClBG,UAAUH,cAIVM,QAAU,CACVuB,WAAY,sCACZC,KAAM,CACFC,OAAQ7B,iBAAiBF,SACzBgC,gBAAiBjC,UAAUC,WAGnCK,YAAYL,QAASM,SAASa,MAAK,YA5FtB,SAASnB,SACtBA,QAAQC,KAAK,kBAAmB,KA4F5BgC,CAAWjC,SACXJ,UAAUsB,OAAO,gCAAiC,IAAIC,MAAK,SAASC,KAAMC,IACtEzB,UAAUsC,oBAAoBlC,QAASoB,KAAMC,WAyD7Cc,CAAWnC,SA5CH,SAASA,aACrBG,UAAUH,cAIVM,QAAU,CACVuB,WAAY,+BACZC,KAAM,CACFM,QAAS,CAACrC,UAAUC,YAI5BK,YAAYL,QAASM,SAASa,MAAK,YA9GnB,SAASnB,SACzBA,QAAQC,KAAK,kBAAmB,KA8G5BoC,CAAcrC,SACdJ,UAAUsB,OAAO,6BAA8B,IAAIC,MAAK,SAASC,KAAMC,IACnEzB,UAAUsC,oBAAoBlC,QAASoB,KAAMC,WA2B7CiB,CAActC,SAIlB0B,EAAEa,iBACFZ,KAAKa,cAAcD"} \ No newline at end of file +{"version":3,"file":"toggle_contact_button.min.js","sources":["../src/toggle_contact_button.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to add/remove contact using ajax.\n *\n * @module core_message/toggle_contact_button\n * @copyright 2016 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/custom_interaction_events'],\n function($, Ajax, Templates, Notification, CustomEvents) {\n\n /**\n * Check the state of the element, if the current user is a contact or not.\n *\n * @method isContact\n * @param {object} element jQuery object for the button\n * @return {bool}\n */\n var isContact = function(element) {\n return element.attr('data-is-contact') == '1';\n };\n\n /**\n * Record that the user is a contact.\n *\n * @method setContact\n * @param {object} element jQuery object for the button\n */\n var setContact = function(element) {\n element.attr('data-is-contact', '1');\n };\n\n /**\n * Record that the user is not a contact.\n *\n * @method setNotContact\n * @param {object} element jQuery object for the button\n */\n var setNotContact = function(element) {\n element.attr('data-is-contact', '0');\n };\n\n /**\n * Get the id for the user being viewed.\n *\n * @method getUserId\n * @param {object} element jQuery object for the button\n * @return {int}\n */\n var getUserId = function(element) {\n return element.attr('data-userid');\n };\n\n /**\n * Get the id for the logged in user.\n *\n * @method getUserId\n * @param {object} element jQuery object for the button\n * @return {int}\n */\n var getCurrentUserId = function(element) {\n return element.attr('data-currentuserid');\n };\n\n /**\n * Check whether a text label should be displayed or not.\n *\n * @method getUserId\n * @param {object} element jQuery object for the button\n * @return {int}\n */\n var displayTextLabel = function(element) {\n return element.attr('data-display-text-label') == '1';\n };\n\n /**\n * Check if this element is currently loading.\n *\n * @method isLoading\n * @param {object} element jQuery object for the button\n * @return {bool}\n */\n var isLoading = function(element) {\n return element.hasClass('loading') || element.attr('disabled');\n };\n\n /**\n * Sends an ajax request to the server and handles the element state\n * while the request is being performed.\n *\n * @method sendRequest\n * @param {object} element jQuery object for the button\n * @param {object} request Request hash to send\n * @return {object} jQuery promise\n */\n var sendRequest = function(element, request) {\n if (isLoading(element)) {\n return $.Deferred();\n }\n\n element.addClass('loading');\n element.attr('disabled', 'disabled');\n\n return Ajax.call([request])[0]\n .fail(Notification.exception)\n .always(function() {\n element.removeClass('loading');\n element.removeAttr('disabled');\n });\n };\n\n /**\n * Send a request to the server to add the current user as\n * a contact. The contents of the button are changed to the\n * remove contact button upon success.\n *\n * @method addContact\n * @param {object} element jQuery object for the button\n */\n var addContact = function(element) {\n if (isLoading(element)) {\n return;\n }\n\n var request = {\n methodname: 'core_message_create_contact_request',\n args: {\n userid: getCurrentUserId(element),\n requesteduserid: getUserId(element),\n }\n };\n sendRequest(element, request).done(function() {\n setContact(element);\n const templateContext = {\n 'displaytextlabel': displayTextLabel(element)\n };\n Templates.render('message/remove_contact_button', templateContext).done(function(html, js) {\n Templates.replaceNodeContents(element, html, js);\n });\n });\n };\n\n /**\n * Send a request to the server to remove the current user as\n * a contact. The contents of the button are changed to the\n * add contact button upon success.\n *\n * @method removeContact\n * @param {object} element jQuery object for the button\n */\n var removeContact = function(element) {\n if (isLoading(element)) {\n return;\n }\n\n var request = {\n methodname: 'core_message_delete_contacts',\n args: {\n userids: [getUserId(element)],\n }\n };\n\n sendRequest(element, request).done(function() {\n setNotContact(element);\n const templateContext = {\n 'displaytextlabel': displayTextLabel(element)\n };\n Templates.render('message/add_contact_button', templateContext).done(function(html, js) {\n Templates.replaceNodeContents(element, html, js);\n });\n });\n };\n\n /**\n * Enhances the given element with a loading gif and event handles to make\n * ajax requests to add or remove a contact where appropriate.\n *\n * @public\n * @method enhance\n * @param {object} element jQuery object for the button\n */\n var enhance = function(element) {\n element = $(element);\n\n if (!element.children('.loading-icon').length) {\n // Add the loading gif if it isn't already there.\n Templates.render('core/loading', {}).done(function(html, js) {\n element.append(html, js);\n });\n }\n\n CustomEvents.define(element, [CustomEvents.events.activate]);\n\n element.on(CustomEvents.events.activate, function(e, data) {\n if (isContact(element)) {\n removeContact(element);\n } else {\n addContact(element);\n }\n e.preventDefault();\n data.originalEvent.preventDefault();\n });\n };\n\n return {\n enhance: enhance\n };\n});\n"],"names":["define","$","Ajax","Templates","Notification","CustomEvents","getUserId","element","attr","getCurrentUserId","displayTextLabel","isLoading","hasClass","sendRequest","request","Deferred","addClass","call","fail","exception","always","removeClass","removeAttr","enhance","children","length","render","done","html","js","append","events","activate","on","e","data","isContact","methodname","args","userid","requesteduserid","setContact","templateContext","replaceNodeContents","addContact","userids","setNotContact","removeContact","preventDefault","originalEvent"],"mappings":";;;;;;;AAsBAA,4CAAO,CAAC,SAAU,YAAa,iBAAkB,oBAAqB,mCAC9D,SAASC,EAAGC,KAAMC,UAAWC,aAAcC,kBAwC3CC,UAAY,SAASC,gBACdA,QAAQC,KAAK,gBAUpBC,iBAAmB,SAASF,gBACrBA,QAAQC,KAAK,uBAUpBE,iBAAmB,SAASH,eACsB,KAA3CA,QAAQC,KAAK,4BAUpBG,UAAY,SAASJ,gBACdA,QAAQK,SAAS,YAAcL,QAAQC,KAAK,aAYnDK,YAAc,SAASN,QAASO,gBAC5BH,UAAUJ,SACHN,EAAEc,YAGbR,QAAQS,SAAS,WACjBT,QAAQC,KAAK,WAAY,YAElBN,KAAKe,KAAK,CAACH,UAAU,GACvBI,KAAKd,aAAae,WAClBC,QAAO,WACJb,QAAQc,YAAY,WACpBd,QAAQe,WAAW,uBAiGxB,CACHC,QAxBU,SAAShB,UACnBA,QAAUN,EAAEM,UAECiB,SAAS,iBAAiBC,QAEnCtB,UAAUuB,OAAO,eAAgB,IAAIC,MAAK,SAASC,KAAMC,IACrDtB,QAAQuB,OAAOF,KAAMC,OAI7BxB,aAAaL,OAAOO,QAAS,CAACF,aAAa0B,OAAOC,WAElDzB,QAAQ0B,GAAG5B,aAAa0B,OAAOC,UAAU,SAASE,EAAGC,OA/KzC,SAAS5B,eACqB,KAAnCA,QAAQC,KAAK,mBA+KZ4B,CAAU7B,SA3EL,SAASA,aAClBI,UAAUJ,cAIVO,QAAU,CACVuB,WAAY,sCACZC,KAAM,CACFC,OAAQ9B,iBAAiBF,SACzBiC,gBAAiBlC,UAAUC,WAGnCM,YAAYN,QAASO,SAASa,MAAK,YAvGtB,SAASpB,SACtBA,QAAQC,KAAK,kBAAmB,KAuG5BiC,CAAWlC,eACLmC,gBAAkB,kBACAhC,iBAAiBH,UAEzCJ,UAAUuB,OAAO,gCAAiCgB,iBAAiBf,MAAK,SAASC,KAAMC,IACnF1B,UAAUwC,oBAAoBpC,QAASqB,KAAMC,WA4D7Ce,CAAWrC,SA/CH,SAASA,aACrBI,UAAUJ,cAIVO,QAAU,CACVuB,WAAY,+BACZC,KAAM,CACFO,QAAS,CAACvC,UAAUC,YAI5BM,YAAYN,QAASO,SAASa,MAAK,YA5HnB,SAASpB,SACzBA,QAAQC,KAAK,kBAAmB,KA4H5BsC,CAAcvC,eACRmC,gBAAkB,kBACAhC,iBAAiBH,UAEzCJ,UAAUuB,OAAO,6BAA8BgB,iBAAiBf,MAAK,SAASC,KAAMC,IAChF1B,UAAUwC,oBAAoBpC,QAASqB,KAAMC,WA2B7CkB,CAAcxC,SAIlB2B,EAAEc,iBACFb,KAAKc,cAAcD"} \ No newline at end of file diff --git a/message/amd/src/toggle_contact_button.js b/message/amd/src/toggle_contact_button.js index 4490494c0cc..9ece821944f 100644 --- a/message/amd/src/toggle_contact_button.js +++ b/message/amd/src/toggle_contact_button.js @@ -76,6 +76,17 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust return element.attr('data-currentuserid'); }; + /** + * Check whether a text label should be displayed or not. + * + * @method getUserId + * @param {object} element jQuery object for the button + * @return {int} + */ + var displayTextLabel = function(element) { + return element.attr('data-display-text-label') == '1'; + }; + /** * Check if this element is currently loading. * @@ -134,7 +145,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust }; sendRequest(element, request).done(function() { setContact(element); - Templates.render('message/remove_contact_button', {}).done(function(html, js) { + const templateContext = { + 'displaytextlabel': displayTextLabel(element) + }; + Templates.render('message/remove_contact_button', templateContext).done(function(html, js) { Templates.replaceNodeContents(element, html, js); }); }); @@ -162,7 +176,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust sendRequest(element, request).done(function() { setNotContact(element); - Templates.render('message/add_contact_button', {}).done(function(html, js) { + const templateContext = { + 'displaytextlabel': displayTextLabel(element) + }; + Templates.render('message/add_contact_button', templateContext).done(function(html, js) { Templates.replaceNodeContents(element, html, js); }); }); diff --git a/message/classes/helper.php b/message/classes/helper.php index 1396d8f38c9..9a66ca2eddc 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -308,14 +308,16 @@ class helper { * * @param object $user User object. * @param bool $iscontact + * @param bool $displaytextlabel Instructs whether to display a text label. * @return array */ - public static function togglecontact_link_params($user, $iscontact = false) { + public static function togglecontact_link_params($user, $iscontact = false, bool $displaytextlabel = true) { global $USER; $params = array( 'data-currentuserid' => $USER->id, 'data-userid' => $user->id, 'data-is-contact' => $iscontact, + 'data-display-text-label' => $displaytextlabel, 'id' => 'toggle-contact-button', 'role' => 'button', 'class' => 'ajax-contact-button', diff --git a/message/templates/add_contact_button.mustache b/message/templates/add_contact_button.mustache index c06d6614817..f4972a58d98 100644 --- a/message/templates/add_contact_button.mustache +++ b/message/templates/add_contact_button.mustache @@ -20,14 +20,15 @@ Template for the contents of the add contact button on the user's profile page. Context variables required for this template: - * none + * displaytextlabel - Whether to display text next to the action icon. Example context (json): { + "displaytextlabel": true } }} - {{#pix}} t/addcontact, core {{/pix}} - {{#str}} addtoyourcontacts, message {{/str}} + {{#pix}} t/addcontact, core, {{#str}} addtoyourcontacts, message {{/str}} {{/pix}} + {{#displaytextlabel}}{{#str}} addtoyourcontacts, message {{/str}}{{/displaytextlabel}} {{> core/loading }} diff --git a/message/templates/remove_contact_button.mustache b/message/templates/remove_contact_button.mustache index 2f542f55c2a..70b9fda103b 100644 --- a/message/templates/remove_contact_button.mustache +++ b/message/templates/remove_contact_button.mustache @@ -20,14 +20,15 @@ Template for the contents of the add contact button on the user's profile page. Context variables required for this template: - * none + * displaytextlabel - Whether to display text next to the action icon. Example context (json): { + "displaytextlabel": true } }} - {{#pix}} t/removecontact, core {{/pix}} - {{#str}} removefromyourcontacts, message {{/str}} + {{#pix}} t/removecontact, core, {{#str}} removefromyourcontacts, message {{/str}} {{/pix}} + {{#displaytextlabel}}{{#str}} removefromyourcontacts, message {{/str}}{{/displaytextlabel}} {{> core/loading }} diff --git a/mod/lesson/tests/behat/link_to_gradebook.feature b/mod/lesson/tests/behat/link_to_gradebook.feature index 4f1b67c549c..37904f953aa 100644 --- a/mod/lesson/tests/behat/link_to_gradebook.feature +++ b/mod/lesson/tests/behat/link_to_gradebook.feature @@ -49,7 +49,8 @@ Feature: link to gradebook on the end of lesson page Then I should see "Congratulations - end of lesson reached" And I should see "View grades" And I follow "View grades" - And I should see "User report - Student 1" + And I should see "User report" in the "page-header" "region" + And I should see "Student 1" in the "region-main" "region" And I should see "Test lesson" Scenario: No link to gradebook for non graded lesson diff --git a/theme/boost/scss/moodle/grade.scss b/theme/boost/scss/moodle/grade.scss index d4400783a9c..0a6f2fa4f79 100644 --- a/theme/boost/scss/moodle/grade.scss +++ b/theme/boost/scss/moodle/grade.scss @@ -65,6 +65,11 @@ #region-main { overflow-x: visible; } + + .user-heading .userinitials { + width: 50px; + height: 50px; + } } // Rubrics diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index ff86f5a0379..6e0baae01b3 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -19417,6 +19417,10 @@ p.arrow_button { .path-grade #region-main { overflow-x: visible; } +.path-grade .user-heading .userinitials { + width: 50px; + height: 50px; } + #page-grade-grading-manage #activemethodselector label { display: inline-block; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 0ffdca30bab..65c074274ce 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -19417,6 +19417,10 @@ p.arrow_button { .path-grade #region-main { overflow-x: visible; } +.path-grade .user-heading .userinitials { + width: 50px; + height: 50px; } + #page-grade-grading-manage #activemethodselector label { display: inline-block; }