1
0
mirror of https://github.com/moodle/moodle.git synced 2025-05-12 03:05:42 +02:00

Merge branch 'MDL-75513-master-3' of https://github.com/mihailges/moodle

This commit is contained in:
Jun Pataleta 2022-11-02 21:34:03 +08:00 committed by Ilya Tregubov
commit 486645f487
47 changed files with 1459 additions and 208 deletions

@ -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
*

@ -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);
}
}

@ -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 <mihail@moodle.com>
* @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

File diff suppressed because one or more lines are too long

@ -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 <http://www.gnu.org/licenses/>.
/**
* Javascript module for toggling the visibility of the grade categories in the user report.
*
* @module gradereport_user/gradecategorytoggle
* @copyright 2022 Mihail Geshoski <mihail@moodle.com>
* @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);
};

@ -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'
)
)

@ -0,0 +1,98 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace 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 <mihail@moodle.com>
* @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;
}
}

@ -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 = "
<table cellspacing='0'
cellpadding='0'
summary='" . s($this->get_lang_string('tablesummary', 'gradereport_user')) . "'
class='boxaligncenter generaltable user-grade'>
<thead>
<tr>
<th id='".$this->tablecolumns[0]."' class=\"header column-{$this->tablecolumns[0]}\" colspan='$maxspan'>".$this->tableheaders[0]."</th>\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 .= "<th id='".$this->tablecolumns[$i]."' class=\"header column-{$this->tablecolumns[$i]}\">".$this->tableheaders[$i]."</th>\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 .= "
</tr>
</thead>
<tbody>\n";
// Print out the table data.
for ($i = 0; $i < count($this->tabledata); $i++) {
$html .= "<tr>\n";
if (isset($this->tabledata[$i]['leader'])) {
$rowspan = $this->tabledata[$i]['leader']['rowspan'];
$class = $this->tabledata[$i]['leader']['class'];
$html .= "<td class='$class' rowspan='$rowspan'></td>\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</$celltype>\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 .= "</tr>\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 .= "</tbody></table>";
$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);
}

@ -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 '<br />' . $report->print_table(true);
echo $report->print_table(true);
}
echo "<p style = 'page-break-after: always;'></p>";
}
$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 '<br />' . $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 '<br />' . $report->print_table(true);
echo $report->print_table(true);
}
}

@ -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';

@ -0,0 +1,9 @@
<svg width="128" height="119" viewBox="0 0 128 119" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M54.48 108.96C84.5685 108.96 108.96 84.5685 108.96 54.48C108.96 24.3915 84.5685 0 54.48 0C24.3915 0 0 24.3915 0 54.48C0 84.5685 24.3915 108.96 54.48 108.96Z" fill="#C4E5FF"/>
<path d="M73.6898 29.0098H33.5398C30.4856 29.0098 28.0098 31.4856 28.0098 34.5398V34.5498C28.0098 37.6039 30.4856 40.0798 33.5398 40.0798H73.6898C76.7439 40.0798 79.2198 37.6039 79.2198 34.5498V34.5398C79.2198 31.4856 76.7439 29.0098 73.6898 29.0098Z" fill="#85CAFF"/>
<path d="M73.6898 72.4297H33.5398C30.4856 72.4297 28.0098 74.9056 28.0098 77.9597V77.9697C28.0098 81.0238 30.4856 83.4997 33.5398 83.4997H73.6898C76.7439 83.4997 79.2198 81.0238 79.2198 77.9697V77.9597C79.2198 74.9056 76.7439 72.4297 73.6898 72.4297Z" fill="#85CAFF"/>
<path d="M96.6497 47.9404H36.3297C31.7347 47.9404 28.0098 51.6632 28.0098 56.2554C28.0098 60.8477 31.7347 64.5704 36.3297 64.5704H96.6497C101.245 64.5704 104.97 60.8477 104.97 56.2554C104.97 51.6632 101.245 47.9404 96.6497 47.9404Z" fill="#85CAFF" stroke="#0056B2" stroke-width="2" stroke-miterlimit="10"/>
<path d="M40.9306 59.9601C40.9276 59.9169 40.9276 59.8734 40.9306 59.8301V59.7101L40.8706 59.5801L40.8106 59.4702C40.7654 59.4004 40.7116 59.3366 40.6506 59.2802L40.5606 59.2001L40.3607 59.0601L40.2506 59.0001C39.9335 58.8481 39.597 58.7404 39.2506 58.6801C38.9327 58.6107 38.6324 58.4774 38.3676 58.2883C38.1029 58.0992 37.8794 57.8583 37.7107 57.5801C37.6981 57.4198 37.7211 57.2586 37.7782 57.1082C37.8352 56.9577 37.9248 56.8218 38.0406 56.7101L38.1306 56.6002C38.3931 56.251 38.565 55.8421 38.6306 55.4102C38.6306 55.4102 38.6306 55.3401 38.6306 55.3101C38.7701 54.6878 38.7701 54.0424 38.6306 53.4201C38.5121 53.014 38.2743 52.6528 37.948 52.3835C37.6216 52.1143 37.2219 51.9494 36.8006 51.9102H36.5106H36.2207H36.1606C35.7394 51.9494 35.3396 52.1143 35.0133 52.3835C34.6869 52.6528 34.4491 53.014 34.3307 53.4201C34.1959 54.0429 34.1959 54.6873 34.3307 55.3101C34.326 55.3433 34.326 55.377 34.3307 55.4102C34.387 55.8446 34.5598 56.2558 34.8307 56.6002C34.854 56.6391 34.8807 56.6759 34.9106 56.7101C35.0264 56.8218 35.1161 56.9577 35.1732 57.1082C35.2302 57.2586 35.2533 57.4198 35.2407 57.5801C35.0726 57.8569 34.8505 58.0969 34.5876 58.2859C34.3247 58.4749 34.0265 58.6089 33.7107 58.6801C33.3644 58.7412 33.028 58.8489 32.7107 59.0001L32.6107 59.0601C32.5433 59.1011 32.4797 59.1479 32.4206 59.2001L32.3407 59.2802C32.2859 59.3395 32.2357 59.4031 32.1906 59.4702L32.1306 59.5801L32.0706 59.7101C32.0667 59.75 32.0667 59.7902 32.0706 59.8301C32.065 59.8733 32.065 59.917 32.0706 59.9601C32.0741 59.9867 32.0741 60.0135 32.0706 60.0401C32.6082 60.766 33.3084 61.3558 34.1151 61.7622C34.9217 62.1686 35.8124 62.3803 36.7157 62.3803C37.6189 62.3803 38.5096 62.1686 39.3163 61.7622C40.1229 61.3558 40.8231 60.766 41.3607 60.0401C41.3607 60.0401 41.3607 59.9801 41.3607 59.9601H40.9306Z" fill="white"/>
<path d="M122.86 83.0704C122.534 82.7264 122.14 82.4544 121.702 82.2719C121.265 82.0893 120.794 82.0003 120.32 82.0104C119.858 82.001 119.398 82.0846 118.969 82.2564C118.539 82.4282 118.149 82.6846 117.82 83.0104C117.483 83.3324 117.215 83.7206 117.034 84.1508C116.854 84.581 116.764 85.0438 116.77 85.5104V86.5104H115.77V82.5104C115.774 81.6779 115.492 80.8694 114.97 80.2204C114.483 79.5632 113.771 79.1077 112.97 78.9404C112.729 78.8998 112.485 78.8797 112.24 78.8804C111.3 78.8804 110.399 79.253 109.733 79.9166C109.068 80.5802 108.693 81.4806 108.69 82.4203V86.4203H107.69V82.5604C107.712 81.5268 107.357 80.5207 106.69 79.7303C106.368 79.3381 105.969 79.0147 105.519 78.7794C105.069 78.544 104.576 78.4015 104.07 78.3604C103.934 78.3477 103.797 78.3477 103.66 78.3604C103.126 78.3526 102.596 78.4542 102.102 78.6589C101.609 78.8635 101.162 79.1669 100.79 79.5504C100.407 79.9199 100.104 80.3648 99.9011 80.857C99.698 81.3492 99.599 81.8781 99.6102 82.4104V86.4104H98.6102V73.6404V59.4404C98.6211 58.6568 98.3751 57.8912 97.9101 57.2604C97.8107 57.1158 97.6969 56.9817 97.5702 56.8604C96.967 56.2522 96.156 55.8949 95.3001 55.8604H94.8202C93.9643 55.8949 93.1533 56.2522 92.5501 56.8604C92.4262 56.9843 92.3126 57.1181 92.2102 57.2604C91.7452 57.8912 91.4992 58.6568 91.5101 59.4404V83.4404C91.5139 83.497 91.5139 83.5537 91.5101 83.6104V97.1804L90.5101 96.2303V90.7903C90.5224 90.3155 90.4403 89.843 90.2686 89.4001C90.097 88.9573 89.8392 88.5529 89.5101 88.2104C89.1829 87.8679 88.7883 87.5969 88.3512 87.4144C87.914 87.232 87.4438 87.1421 86.9702 87.1504C86.5045 87.1498 86.0433 87.2423 85.614 87.4227C85.1846 87.603 84.7958 87.8674 84.4702 88.2004C84.1335 88.5231 83.8666 88.9115 83.686 89.3415C83.5054 89.7715 83.4149 90.234 83.4201 90.7004V97.7903C83.4174 98.2751 83.5146 98.7553 83.7055 99.2009C83.8965 99.6465 84.1772 100.048 84.5301 100.38L94.3801 109.74C94.795 110.164 95.119 110.669 95.3322 111.222C95.5455 111.776 95.6434 112.368 95.6201 112.96C95.6212 113.225 95.6748 113.487 95.7778 113.731C95.8808 113.974 96.0312 114.195 96.2202 114.38C96.4037 114.574 96.6257 114.728 96.8719 114.831C97.1181 114.934 97.3831 114.985 97.6501 114.98H117.95C118.481 114.98 118.989 114.77 119.364 114.395C119.739 114.02 119.95 113.511 119.95 112.98V112.07C119.943 111.247 120.047 110.426 120.26 109.63L123.67 95.8404C123.886 95.0452 123.994 94.2244 123.99 93.4004V85.6204C123.982 85.1414 123.877 84.6691 123.683 84.2311C123.489 83.7932 123.209 83.3985 122.86 83.0704Z" fill="white"/>
<path d="M127.94 85.6397C127.955 84.6323 127.765 83.6323 127.382 82.7006C126.999 81.7688 126.43 80.9248 125.71 80.2197C124.773 79.2918 123.613 78.6205 122.342 78.2703C121.07 77.92 119.73 77.9026 118.45 78.2197C117.345 76.6425 115.685 75.5419 113.802 75.1393C111.919 74.7368 109.953 75.062 108.3 76.0497C106.946 75.0067 105.279 74.4502 103.57 74.4697C103.236 74.4696 102.902 74.4896 102.57 74.5297V59.4397C102.586 58.4331 102.397 57.4337 102.016 56.5021C101.634 55.5704 101.068 54.7259 100.35 54.0197C99.3935 53.0386 98.1809 52.3457 96.85 52.0197H96.7C96.5251 51.98 96.3482 51.9499 96.17 51.9297H95.85C95.59 51.9297 95.32 51.9297 95.04 51.9297H94.9C94.63 51.9297 94.36 51.9297 94.09 51.9297H93.78C93.6017 51.9491 93.4246 51.9792 93.2499 52.0197H93.09C91.7613 52.3439 90.5514 53.0372 89.6 54.0197C88.8812 54.7254 88.3129 55.5696 87.9296 56.5012C87.5463 57.4328 87.3559 58.4324 87.3699 59.4397V83.0697C87.2069 83.0556 87.043 83.0556 86.8799 83.0697C84.8667 83.075 82.9372 83.8757 81.5118 85.2974C80.0863 86.7191 79.2805 88.6465 79.2699 90.6597V97.7497C79.2589 98.7902 79.4689 99.8213 79.886 100.775C80.3032 101.728 80.9181 102.582 81.69 103.28L91.47 112.64C91.4322 114.308 92.0455 115.926 93.18 117.15C93.736 117.752 94.4147 118.228 95.1703 118.545C95.9259 118.862 96.7408 119.014 97.56 118.99H117.86C118.662 119.002 119.459 118.853 120.202 118.552C120.946 118.251 121.622 117.803 122.19 117.237C122.757 116.67 123.206 115.995 123.508 115.251C123.811 114.508 123.961 113.712 123.95 112.91C123.926 112.155 123.99 111.4 124.14 110.66L127.58 96.8697C127.842 95.7483 127.98 94.6013 127.99 93.4497L127.94 85.6397ZM123.57 95.8697L120.18 109.66C119.967 110.455 119.862 111.276 119.87 112.1V112.89C119.87 113.42 119.659 113.929 119.284 114.304C118.909 114.679 118.4 114.89 117.87 114.89H97.57C97.303 114.895 97.0379 114.844 96.7917 114.74C96.5454 114.637 96.3235 114.484 96.1399 114.29C95.951 114.104 95.8007 113.884 95.6977 113.64C95.5947 113.396 95.5411 113.134 95.54 112.87C95.5632 112.277 95.4653 111.686 95.2521 111.132C95.0389 110.578 94.7149 110.074 94.3 109.65L84.45 100.29C84.097 99.9573 83.8163 99.5559 83.6253 99.1103C83.4343 98.6647 83.3373 98.1845 83.34 97.6997V90.6097C83.3348 90.1433 83.4252 89.6809 83.6058 89.2509C83.7864 88.8208 84.0533 88.4324 84.3899 88.1097C84.7155 87.7767 85.1045 87.5123 85.5339 87.332C85.9632 87.1516 86.4243 87.0591 86.8899 87.0597C87.3636 87.0514 87.8338 87.1414 88.2709 87.3238C88.7081 87.5062 89.1027 87.7772 89.43 88.1197C89.7591 88.4622 90.0169 88.8666 90.1885 89.3095C90.3602 89.7524 90.4423 90.2249 90.43 90.6997V96.1397L91.43 97.0897V83.5597C91.4337 83.5031 91.4337 83.4463 91.43 83.3897V59.3897C91.4191 58.6061 91.6649 57.8405 92.1299 57.2097C92.2323 57.0675 92.3461 56.9337 92.47 56.8097C93.0732 56.2016 93.8842 55.8443 94.74 55.8097H95.22C96.0758 55.8443 96.8869 56.2016 97.49 56.8097C97.6167 56.9311 97.7306 57.0651 97.83 57.2097C98.295 57.8405 98.5408 58.6061 98.53 59.3897V73.5897V86.5397H99.53V82.5397C99.5188 82.0074 99.6178 81.4785 99.8209 80.9863C100.024 80.4941 100.327 80.0492 100.71 79.6797C101.082 79.2962 101.528 78.9928 102.022 78.7882C102.516 78.5836 103.046 78.482 103.58 78.4897C103.716 78.477 103.854 78.477 103.99 78.4897C104.496 78.5308 104.989 78.6733 105.439 78.9087C105.889 79.144 106.287 79.4674 106.61 79.8597C107.276 80.65 107.632 81.6561 107.61 82.6897V86.5497H108.61V82.5497C108.613 81.6099 108.988 80.7096 109.653 80.046C110.319 79.3824 111.22 79.0097 112.16 79.0097C112.405 79.0091 112.649 79.0291 112.89 79.0697C113.691 79.237 114.403 79.6925 114.89 80.3497C115.411 80.9987 115.694 81.8072 115.69 82.6397V86.6397H116.69V85.6397C116.684 85.1732 116.773 84.7103 116.954 84.2802C117.135 83.85 117.402 83.4617 117.74 83.1397C118.068 82.8139 118.459 82.5575 118.888 82.3857C119.318 82.214 119.778 82.1303 120.24 82.1397C120.714 82.1296 121.185 82.2186 121.622 82.4012C122.059 82.5837 122.454 82.8557 122.78 83.1997C123.109 83.5422 123.367 83.9466 123.539 84.3895C123.71 84.8323 123.792 85.3049 123.78 85.7797V93.5597C123.808 94.3355 123.738 95.1117 123.57 95.8697Z" fill="#0056B2"/>
</svg>

After

(image error) Size: 9.3 KiB

@ -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));
}
}

@ -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;
}
}

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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": "<div class='group-selector'></div>",
"userselector": "<div class='user-selector'></div>",
"viewasselector": "<div class='view-user-selector'></div>"
}
}}
<div class="container-fluid tertiary-navigation full-width-bottom-border">
<div class="row">
{{#generalnavselector}}
<div class="navitem">
{{>core/tertiary_navigation_selector}}
</div>
<div class="navitem-divider"></div>
{{/generalnavselector}}
{{#groupselector}}
<div class="navitem">
{{{groupselector}}}
</div>
<div class="navitem-divider"></div>
{{/groupselector}}
{{#userselector}}
<div class="navitem">
{{{userselector}}}
</div>
<div class="navitem-divider"></div>
{{/userselector}}
<div class="d-flex ml-sm-auto">
{{#viewasselector}}
<div class="navitem-divider"></div>
<div class="navitem">
{{{viewasselector}}}
</div>
<div class="navitem-divider"></div>
{{/viewasselector}}
</div>
</div>
</div>

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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"
}
}
}}
<div class="user-navigation w-100">
<div class="container w-100 d-flex">
{{#previoususer}}
<div class="previous d-flex">
<a href="{{url}}" aria-label="{{#str}} gotopreviousreport, gradereport_user {{/str}}">
<i class="fa fa-caret-left fa-lg pr-1"></i>
{{name}}
</a>
</div>
{{/previoususer}}
{{#nextuser}}
<div class="next d-flex ml-auto">
<a href="{{url}}" aria-label="{{#str}} gotonextreport, gradereport_user {{/str}}">
{{name}}
<i class="fa fa-caret-right fa-lg pl-1"></i>
</a>
</div>
{{/nextuser}}
</div>
</div>

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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"
}
}}
<div class="d-flex category-content">
<a aria-expanded="true" role="button" data-categoryid={{categoryid}} data-target=".cat_{{categoryid}}[data-hidden='false']" class="btn btn-icon mr-1 toggle-category" href="#">
<span class="collapsed text-nowrap" title="{{#str}} collapse, core {{/str}}">
<i class="icon fa fa-chevron-down fa-fw" title="{{#str}} collapse, core {{/str}}" role="img" aria-label="{{#str}} collapse, core {{/str}}"></i>
</span>
<span class="expanded text-nowrap" title="{{#str}} expand, core {{/str}}">
<i class="icon fa fa-chevron-right fa-fw" title="{{#str}} expand, core {{/str}}" role="img" aria-label="{{#str}} expand, core {{/str}}"></i>
</span>
</a>
<span>{{categoryname}}</span>
</div>

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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": "<img src=\"http://example.com/pluginfile.php/14/user/icon/boost/f2\" class=\"userpicture\" width=\"40\" alt=\"\">",
"text": "John Doe",
"additionaltext": "johndoe@example.com"
}
}
}}
<div class="user-selector d-flex">
<div role="button" class="userwidget btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" data-groupid="{{groupid}}" data-searchtype="user" tabindex="0" aria-label="{{#str}} selectauser, core_grades {{/str}}">
<div class="align-items-center d-flex">
{{#selectedoption}}
<div class="selected-option-img d-block pr-2">
{{#image}}
{{{image}}}
{{/image}}
{{^image}}
<span class="userinitials"></span>
{{/image}}
</div>
<div class="selected-option-info d-block pr-3">
<span class="selected-option-text p-0 font-weight-bold">
{{text}}
</span>
{{#additionaltext}}
<span class="d-block small">
{{additionaltext}}
</span>
{{/additionaltext}}
</div>
{{/selectedoption}}
{{^selectedoption}}
<div class="d-block pr-2">
<span class="userinitials"></span>
</div>
<div class="user-info d-block pr-3">
{{#str}} selectauser, core_grades {{/str}}
</div>
{{/selectedoption}}
</div>
</div>
</div>

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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
}
]
}
}}
<div class="view-user-selector">
{{>core/select_menu}}
</div>
{{#js}}
document.querySelector('#{{baseid}}').addEventListener('change', function(e) {
window.location.href = e.target.value;
});
{{/js}}

@ -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"
}
}}
<div class="mx-auto w-50 text-center">
<img src="{{imglink}}" alt="Temp" class="w-50 h-50 mb-4">
<div class="zero-state mx-auto w-50 text-center my-6">
<img src="{{imglink}}" alt="{{#str}}selectuser, gradereport_user{{/str}}" role="presentation" class="my-5">
<h3>{{#str}}selectuser, gradereport_user{{/str}}</h3>
<p>{{#str}}selectuserinstructions, gradereport_user{{/str}}</p>
{{#groupmodeenabled}}
<a href="#"
class="btn btn-light border-secondary groupwidget"
data-courseid="{{courseid}}"
data-action-base-url="{{groupactionbaseurl}}"
data-searchtype="group"
>{{#str}}selectgrouplink, grades{{/str}}</a>
{{/groupmodeenabled}}
<a href="#"
class="btn btn-light border-secondary userwidget"
data-courseid="{{courseid}}"
data-groupid="{{groupid}}"
data-searchtype="user"
>{{#str}}selectuserlink, grades{{/str}}</a>
</div>

@ -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"

@ -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"

@ -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 | 0100 | 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 | 0100 | 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 | 0100 | 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 | 0100 | 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 | 0100 | 80.00 % | 13.33 % |

@ -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

@ -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);
}

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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"
}
}}
<div class="group-selector d-flex">
<div role="button" class="groupwidget btn dropdown-toggle d-flex text-left align-items-center p-0" data-courseid="{{courseid}}" data-action-base-url="{{groupactionbaseurl}}" data-searchtype="group" tabindex="0">
<div class="align-items-center d-flex">
<div class="d-block pr-3">
<span class="d-block small">
{{label}}
</span>
<span class="p-0 font-weight-bold">
{{selectedgroup}}
</span>
</div>
</div>
</div>
</div>

@ -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 <http://www.gnu.org/licenses/>.
}}
{{!
@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": "<img src=\"http://example.com/pluginfile.php/14/user/icon/boost/f2\" class=\"userpicture\" width=\"50\" alt=\"\">",
"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"
}
}
]
}
}}
<div class="user-heading d-flex align-items-center">
<div class="d-flex h4">
{{{image}}}
</div>
<div class="d-flex ml-2">
<a class="h4 m-0" href="{{userprofileurl}}">{{name}}</a>
</div>
<div class="d-inline-flex ml-4">
{{#buttons}}
<a {{#linkattributes}} {{name}}="{{value}}" {{/linkattributes}} aria-label="{{title}}" href="{{url}}">
{{#icon}}
<span>
{{#pix}} {{name}}, {{component}}, {{title}} {{/pix}}
</span>
{{/icon}}
</a>
{{/buttons}}
</div>
</div>

@ -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 %) | 020 | 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 % |

@ -88,7 +88,7 @@ Feature: Calculated grade items can be used in the gradebook
| grade item 1 | - | 75.00 | 0100 | 75.00 % | - |
| Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 040 | 93.75 % | - |
| Course total | - | 37.50 | 040 | 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 | 0100 | 65.00 % | - |
@ -105,7 +105,7 @@ Feature: Calculated grade items can be used in the gradebook
| grade item 1 | - | 75.00 | 0100 | 75.00 % | - |
| Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 040 | 93.75 % | - |
| Course total | - | 37.50 | 040 | 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 | 0100 | 65.00 % | - |
@ -155,7 +155,7 @@ Feature: Calculated grade items can be used in the gradebook
| grade item 1 | 71.43 % | 75.00 | 0100 | 75.00 % | 53.57 % |
| calc item | 28.57 % | 37.50 | 040 | 93.75 % | 26.79 % |
| Course total | - | 112.50 | 0140 | 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 | 0100 | 65.00 % | 46.43 % |

@ -89,7 +89,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
| grade item 1 | - | 75.00 | 0100 | 75.00 % | - |
| Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 0100 | 37.50 % | - |
| Course total | - | 37.50 | 0100 | 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 | 0100 | 65.00 % | - |
@ -106,7 +106,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
| grade item 1 | - | 75.00 | 0100 | 75.00 % | - |
| Calc cat totalInclude empty grades. | 100.00 % | 37.50 | 0100 | 37.50 % | - |
| Course total | - | 37.50 | 0100 | 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 | 0100 | 65.00 % | - |
@ -156,7 +156,7 @@ Feature: Gradebook calculations for calculated grade items before the fix 201506
| grade item 1 | 50.00 % | 75.00 | 0100 | 75.00 % | 37.50 % |
| calc item | 50.00 % | 37.50 | 0100 | 37.50 % | 18.75 % |
| Course total | - | 112.50 | 0200 | 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 | 0100 | 65.00 % | 32.50 % |

@ -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 % |

@ -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 | 0100 | 50.00 % | 25.00 % |

@ -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 | 0100 | 50.00 % | - |

@ -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 % |

@ -86,7 +86,7 @@ Feature: We can choose what min or max grade to use when aggregating grades.
| MI 5 | 20.00 % | 100.00 | 0100 | 100.00 % | 20.00 % |
| CAT1 total | 40.00 % | 150.00 | 0200 | 75.00 % | - |
| Course total | - | 350.00 | 0500 | 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 | 0100 | 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 | 0100 | 100.00 % | 25.00 % |
| CAT1 total | 37.50 % | 150.00 | 0150 | 100.00 % | - |
| Course total | - | 350.00 | 0400 | 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 | 550 | 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 | 0200 | 75.00 % | 30.00 % |
| Course total | - | 400.00 | 0500 | 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 | 0200 | 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 | 0200 | 75.00 % | 25.00 % |
| CAT1 total | 33.33 % | 150.00 | 0200 | 75.00 % | - |
| Course total | - | 400.00 | 0600 | 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 | 0100 | 20.00 % | 5.00 % |

@ -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 | 01 | - |
| Course total | 1.00 | 01 | - |
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! | - |

@ -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);
}

@ -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.

@ -5,6 +5,6 @@
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
* @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

File diff suppressed because one or more lines are too long

@ -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);
});
});

@ -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',

@ -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
}
}}
<span>
{{#pix}} t/addcontact, core {{/pix}}
<span class="header-button-title">{{#str}} addtoyourcontacts, message {{/str}}</span>
{{#pix}} t/addcontact, core, {{#str}} addtoyourcontacts, message {{/str}} {{/pix}}
{{#displaytextlabel}}<span class="header-button-title">{{#str}} addtoyourcontacts, message {{/str}}</span>{{/displaytextlabel}}
</span>
{{> core/loading }}

@ -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
}
}}
<span>
{{#pix}} t/removecontact, core {{/pix}}
<span class="header-button-title">{{#str}} removefromyourcontacts, message {{/str}}</span>
{{#pix}} t/removecontact, core, {{#str}} removefromyourcontacts, message {{/str}} {{/pix}}
{{#displaytextlabel}}<span class="header-button-title">{{#str}} removefromyourcontacts, message {{/str}}</span>{{/displaytextlabel}}
</span>
{{> core/loading }}

@ -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

@ -65,6 +65,11 @@
#region-main {
overflow-x: visible;
}
.user-heading .userinitials {
width: 50px;
height: 50px;
}
}
// Rubrics

@ -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; }

@ -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; }