Skip to content

Commit

Permalink
MDL-77030 gradereport_grader: Display feedback in grader report
Browse files Browse the repository at this point in the history
  • Loading branch information
kevpercy committed Apr 11, 2023
1 parent bd2de8e commit b976b3b
Show file tree
Hide file tree
Showing 19 changed files with 587 additions and 4 deletions.

Large diffs are not rendered by default.

114 changes: 114 additions & 0 deletions grade/classes/external/get_feedback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.

namespace core_grades\external;

use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use invalid_parameter_exception;

defined('MOODLE_INTERNAL') || die;

require_once($CFG->dirroot.'/grade/lib.php');

/**
* Web service to fetch students feedback for a grade item.
*
* @package core_grades
* @copyright 2023 Kevin Percy <[email protected]>
* @category external
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_feedback extends external_api {

/**
* Returns description of method parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters (
[
'courseid' => new external_value(PARAM_INT, 'Course ID', VALUE_REQUIRED),
'userid' => new external_value(PARAM_INT, 'User ID', VALUE_REQUIRED),
'itemid' => new external_value(PARAM_INT, 'Grade Item ID', VALUE_REQUIRED)
]
);
}

/**
* Given a user ID and grade item ID, return feedback and user details.
*
* @param int $courseid The course ID.
* @param int $userid
* @param int $itemid
* @return array Feedback and user details
*/
public static function execute(int $courseid, int $userid, int $itemid): array {
global $OUTPUT, $CFG;

$params = self::validate_parameters(
self::execute_parameters(),
[
'courseid' => $courseid,
'userid' => $userid,
'itemid' => $itemid
]
);

$context = \context_course::instance($courseid);
parent::validate_context($context);

require_capability('gradereport/grader:view', $context);

$gtree = new \grade_tree($params['courseid'], false, false, null, !$CFG->enableoutcomes);
$gradeitem = $gtree->get_item($params['itemid']);

// If Item ID is not part of Course ID, $gradeitem will be set to false.
if ($gradeitem === false) {
throw new invalid_parameter_exception('Course ID and item ID mismatch');
}

$grade = $gradeitem->get_grade($params['userid'], false);
$user = \core_user::get_user($params['userid']);
$extrafields = \core_user\fields::get_identity_fields($context);

return [
'feedbacktext' => $grade->feedback,
'title' => $gradeitem->get_name(true),
'fullname' => fullname($user),
'picture' => $OUTPUT->user_picture($user, ['size' => 35, 'link' => false]),
'additionalfield' => empty($extrafields) ? '' : $user->{$extrafields[0]},
];
}

/**
* Describes the return structure.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'feedbacktext' => new external_value(PARAM_RAW, 'The full feedback text'),
'title' => new external_value(PARAM_TEXT, 'Title of the grade item that the feedback is for'),
'fullname' => new external_value(PARAM_TEXT, 'Students name'),
'picture' => new external_value(PARAM_RAW, 'Students picture'),
'additionalfield' => new external_value(PARAM_TEXT, 'Additional field for the user (email or ID number, for example)'),
]);
}
}
18 changes: 17 additions & 1 deletion grade/lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -2464,6 +2464,12 @@ public function set_grade_status_icons(array $element): string {
if (isset($element['type']) && ($element['type'] == 'category')) {
$class = 'category_grade_icons';
}

if (!empty($grade->feedback) && $grade->load_grade_item()->gradetype != GRADE_TYPE_TEXT) {
$statusicons .= $OUTPUT->pix_icon('i/asterisk', grade_helper::get_lang_string('feedbackprovided', 'grades'),
'moodle', $attributes);
}

if ($statusicons) {
$statusicons = $OUTPUT->container($statusicons, $class);
}
Expand All @@ -2489,6 +2495,8 @@ public function get_cell_action_menu(array $element, string $mode, grade_plugin_
$editable = true;

if ($element['type'] == 'grade') {
$context->datatype = 'grade';

$item = $element['object']->grade_item;
if ($item->is_course_item() || $item->is_category_item()) {
$editable = (bool)get_config('moodle', 'grade_overridecat');;
Expand Down Expand Up @@ -2595,6 +2603,13 @@ public function get_cell_action_menu(array $element, string $mode, grade_plugin_
} else if ($element['type'] == 'userfield') {
$context->dataid = $element['name'];
}

if ($element['type'] != 'text' && !empty($element['object']->feedback)) {
$viewfeedbackstring = grade_helper::get_lang_string('viewfeedback', 'grades');
$context->viewfeedbackurl = html_writer::link('#', $viewfeedbackstring, ['class' => 'dropdown-item',
'aria-label' => $viewfeedbackstring, 'role' => 'menuitem', 'data-action' => 'feedback',
'data-courseid' => $this->courseid]);
}
} else if ($mode == 'user') {
$context->datatype = 'user';
$context = grade_report::get_additional_context($this->context, $this->courseid, $element, $gpr, $mode, $context, true);
Expand All @@ -2603,7 +2618,8 @@ public function get_cell_action_menu(array $element, string $mode, grade_plugin_

if (!empty($USER->editing) || isset($context->gradeanalysisurl) || isset($context->gradesonlyurl)
|| isset($context->aggregatesonlyurl) || isset($context->fullmodeurl) || isset($context->reporturl0)
|| isset($context->ascendingfirstnameurl) || isset($context->ascendingurl) || ($mode == 'setup')) {
|| isset($context->ascendingfirstnameurl) || isset($context->ascendingurl)
|| isset($context->viewfeedbackurl) || ($mode == 'setup')) {
return $OUTPUT->render_from_template('core_grades/cellmenu', $context);
}
return '';
Expand Down
10 changes: 10 additions & 0 deletions grade/report/grader/amd/build/feedback_modal.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions grade/report/grader/amd/build/feedback_modal.min.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 107 additions & 0 deletions grade/report/grader/amd/src/feedback_modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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 displaying feedback in a modal window
*
* @module gradereport_grader/feedback_modal
* @copyright 2023 Kevin Percy <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import ModalFactory from 'core/modal_factory';
import ajax from 'core/ajax';
import Templates from 'core/templates';

const Selectors = {
showFeedback: '[data-action="feedback"]'
};

/**
* Create the modal to display the feedback.
*
* @param {int} courseid
* @param {int} userid
* @param {int} itemid
* @returns {Promise}
*/
const getModal = async(courseid, userid, itemid) => {
const feedbackData = await fetchFeedback(courseid, userid, itemid);

return ModalFactory.create({
removeOnClose: true,
large: true
})
.then(modal => {
const body = Templates.render('core_grades/feedback_modal', {
feedbacktext: feedbackData.feedbacktext,
user: {
picture: feedbackData.picture,
fullname: feedbackData.fullname,
additionalfield: feedbackData.additionalfield,
},
});

modal.setBody(body);
modal.setTitle(feedbackData.title);
modal.show();

return modal;
});
};

/**
* Fetch the feedback data.
*
* @param {int} courseid
* @param {int} userid
* @param {int} itemid
* @returns {Promise}
*/
const fetchFeedback = (courseid, userid, itemid) => {
const request = {
methodname: 'core_grades_get_feedback',
args: {
courseid: courseid,
userid: userid,
itemid: itemid,
},
};
return ajax.call([request])[0];
};

/**
* Register event listeners for the View Feedback links.
*/
const registerEventListeners = () => {
document.addEventListener('click', e => {
const showFeedbackTrigger = e.target.closest(Selectors.showFeedback);
if (showFeedbackTrigger) {
e.preventDefault();

const courseid = showFeedbackTrigger.dataset.courseid;
const userid = e.target.closest('tr').dataset.uid;
const itemid = e.target.closest('td').dataset.itemid;

getModal(courseid, userid, itemid);
}
});
};

/**
* Initialize module
*/
export const init = () => {
registerEventListeners();
};
1 change: 1 addition & 0 deletions grade/report/grader/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
$PAGE->set_pagelayout('report');
$PAGE->requires->js_call_amd('gradereport_grader/stickycolspan', 'init');
$PAGE->requires->js_call_amd('gradereport_grader/search', 'init');
$PAGE->requires->js_call_amd('gradereport_grader/feedback_modal', 'init');

// basic access checks
if (!$course = $DB->get_record('course', array('id' => $courseid))) {
Expand Down
7 changes: 6 additions & 1 deletion grade/report/grader/lib.php
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,12 @@ public function get_right_rows(bool $displayaverages) : array {
}
}

if (!$item->needsupdate) {
if ($item->gradetype == GRADE_TYPE_TEXT && !empty($grade->feedback)) {
$context->text = html_writer::span(shorten_text(strip_tags($grade->feedback), 20), '',
['data-action' => 'feedback', 'role' => 'button', 'data-courseid' => $this->courseid]);
}

if (!$item->needsupdate && !($item->gradetype == GRADE_TYPE_TEXT && empty($USER->editing))) {
$context->actionmenu = $this->gtree->get_cell_action_menu($element, 'gradeitem', $this->gpr);
}

Expand Down
2 changes: 2 additions & 0 deletions grade/templates/cellmenu.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"descendingfirstnameurl": "<a class='dropdown-item' aria-label='Descending' role='menuitem' href='index.php?id=13&amp;sortitemid=firstname&amp;sort=desc&amp;gpr_type=report&amp;gpr_plugin=grader&amp;gpr_courseid=13'>Descending</a>",
"ascendinglastnameurl": "<a class='dropdown-item' aria-label='Ascending' role='menuitem' href='index.php?id=13&amp;sortitemid=lastname&amp;sort=asc&amp;gpr_type=report&amp;gpr_plugin=grader&amp;gpr_courseid=13'>Ascending</a>",
"descendinglastnameurl": "<a class='dropdown-item' aria-label='Descending' role='menuitem' href='index.php?id=13&amp;sortitemid=lastname&amp;sort=desc&amp;gpr_type=report&amp;gpr_plugin=grader&amp;gpr_courseid=13'>Descending</a>",
"viewfeedbackurl": "<a href='#' class='dropdown-item' aria-label='View feedback' role='menuitem'>View feedback</a>",
"divider1": "true",
"divider2": "true",
"datatype": "item",
Expand Down Expand Up @@ -78,6 +79,7 @@
{{#hideurl}}{{{hideurl}}}{{/hideurl}}
{{#lockurl}}{{{lockurl}}}{{/lockurl}}
{{#resetweightsurl}}{{{resetweightsurl}}}{{/resetweightsurl}}
{{#viewfeedbackurl}}{{{viewfeedbackurl}}}{{/viewfeedbackurl}}
</div>
</div>
</div>
Loading

0 comments on commit b976b3b

Please sign in to comment.