diff --git a/grade/tests/behat/behat_grades.php b/grade/tests/behat/behat_grades.php
index a9b64aeadc7..3790d0192b3 100644
--- a/grade/tests/behat/behat_grades.php
+++ b/grade/tests/behat/behat_grades.php
@@ -38,6 +38,10 @@ class behat_grades extends behat_base {
'initials bar',
[".//*[contains(concat(' ', @class, ' '), ' initialbar ')]//span[contains(., %locator%)]/parent::div"]
),
+ new behat_component_named_selector(
+ 'grade_actions',
+ ["//td[count(//table[@id='user-grades']//th[contains(., %locator%)]/preceding-sibling::th)]//*[@data-type='grade']"]
+ ),
];
}
diff --git a/grade/tests/behat/grade_feedback.feature b/grade/tests/behat/grade_feedback.feature
new file mode 100644
index 00000000000..161758bad7a
--- /dev/null
+++ b/grade/tests/behat/grade_feedback.feature
@@ -0,0 +1,69 @@
+@gradereport @gradereport_grader @javascript
+Feature: Display feedback on the Grader report
+ In order to check the expected results are displayed
+ As a teacher
+ I need to see the feedback information in a modal
+
+ Background:
+ Given the following "courses" exist:
+ | fullname | shortname |
+ | Course 1 | C1 |
+ And the following "users" exist:
+ | username | firstname | lastname | email |
+ | teacher1 | Teacher | 1 | teacher1@example.com |
+ | student1 | Student | 1 | student1@example.com |
+ | student2 | Student | 2 | student2@example.com |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | teacher1 | C1 | editingteacher |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ And the following "activities" exist:
+ | activity | course | section | name | intro | assignfeedback_comments_enabled |
+ | assign | C1 | 1 | Test assignment name 1 | Submit your online text | 1 |
+ | assign | C1 | 1 | Test assignment name 2 | submit your online text | 1 |
+ And the following "mod_assign > submissions" exist:
+ | assign | user | onlinetext |
+ | Test assignment name 1 | student1 | This is a submission for assignment 1 |
+ | Test assignment name 2 | student1 | This is a submission for assignment 2 |
+ And the following "grade items" exist:
+ | itemname | course | gradetype | itemtype |
+ | Grade item 1 | C1 | text | manual |
+ And the following "grade grades" exist:
+ | gradeitem | user | grade | feedback |
+ | Grade item 1 | student1 | | Longer feedback text content |
+ And I log in as "teacher1"
+ And I am on the "Test assignment name 1" "assign activity" page
+ And I follow "View all submissions"
+ And I click on "Grade" "link" in the "Student 1" "table_row"
+ And I set the following fields to these values:
+ | Grade out of 100 | 50 |
+ | Feedback comments | This is feedback |
+ And I press "Save changes"
+
+ Scenario: View the feedback icon on the Grader report
+ Given I am on "Course 1" course homepage
+ When I navigate to "View > Grader report" in the course gradebook
+ Then I should see "Test assignment name 1"
+ And I should see "Test assignment name 2"
+ And "Feedback provided" "icon" should exist in the "Student 1" "table_row"
+ And "Feedback provided" "icon" should not exist in the "Student 2" "table_row"
+
+ Scenario: View the feedback modal from the action menu
+ Given I am on "Course 1" course homepage
+ And I navigate to "View > Grader report" in the course gradebook
+ And I click on "Test assignment name 1" "core_grades > grade_actions" in the "Student 1" "table_row"
+ When I choose "View feedback" in the open action menu
+ Then I should see "This is feedback" in the "Test assignment name 1" "dialogue"
+
+ Scenario: View the feedback text for text only grade
+ Given I am on "Course 1" course homepage
+ When I navigate to "View > Grader report" in the course gradebook
+ Then I should see "Grade item 1"
+ And "Longer feedback ..." "text" should exist in the "Student 1" "table_row"
+
+ Scenario: View the feedback modal for text only grade
+ Given I am on "Course 1" course homepage
+ And I navigate to "View > Grader report" in the course gradebook
+ When I click on "Longer feedback ..." "text" in the "Student 1" "table_row"
+ Then I should see "Longer feedback text content" in the "Grade item 1" "dialogue"
diff --git a/grade/tests/external/get_feedback_test.php b/grade/tests/external/get_feedback_test.php
new file mode 100644
index 00000000000..b0180301eb4
--- /dev/null
+++ b/grade/tests/external/get_feedback_test.php
@@ -0,0 +1,179 @@
+.
+
+namespace core_grades\external;
+
+defined('MOODLE_INTERNAL') || die;
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * Unit tests for the core_grades\external\get_feedback webservice.
+ *
+ * @package core_grades
+ * @category external
+ * @copyright 2023 Kevin Percy
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since Moodle 4.2
+ */
+class get_feedback_test extends \externallib_advanced_testcase {
+
+ /**
+ * Test get_feedback.
+ *
+ * @covers ::get_feedback
+ * @dataProvider get_feedback_provider
+ * @param string|null $feedback The feedback text added for the grade item.
+ * @param array $expected The expected feedback data.
+ * @return void
+ */
+ public function test_get_feedback(?string $feedback, array $expected) {
+
+ $this->resetAfterTest(true);
+ $course = $this->getDataGenerator()->create_course();
+ $user = $this->getDataGenerator()->create_user(['firstname' => 'John', 'lastname' => 'Doe',
+ 'email' => 'johndoe@example.com']);
+ $this->getDataGenerator()->enrol_user($user->id, $course->id);
+ $gradeitem = $this->getDataGenerator()->create_grade_item(['itemname' => 'Grade item 1',
+ 'courseid' => $course->id]);
+
+ $gradegradedata = [
+ 'itemid' => $gradeitem->id,
+ 'userid' => $user->id,
+ ];
+
+ if ($feedback) {
+ $gradegradedata['feedback'] = $feedback;
+ }
+
+ $this->getDataGenerator()->create_grade_grade($gradegradedata);
+ $this->setAdminUser();
+
+ $feedbackdata = get_feedback::execute($course->id, $user->id, $gradeitem->id);
+
+ $this->assertEquals($expected['feedbacktext'], $feedbackdata['feedbacktext']);
+ $this->assertEquals($expected['title'], $feedbackdata['title']);
+ $this->assertEquals($expected['fullname'], $feedbackdata['fullname']);
+ $this->assertEquals($expected['additionalfield'], $feedbackdata['additionalfield']);
+ }
+
+ /**
+ * Data provider for test_get_feedback().
+ *
+ * @return array
+ */
+ public function get_feedback_provider(): array {
+ return [
+ 'Return when feedback is set.' => [
+ 'Test feedback',
+ [
+ 'feedbacktext' => 'Test feedback',
+ 'title' => 'Grade item 1',
+ 'fullname' => 'John Doe',
+ 'additionalfield' => 'johndoe@example.com'
+ ]
+ ],
+ 'Return when feedback is not set.' => [
+ null,
+ [
+ 'feedbacktext' => null,
+ 'title' => 'Grade item 1',
+ 'fullname' => 'John Doe',
+ 'additionalfield' => 'johndoe@example.com'
+ ]
+ ]
+ ];
+ }
+
+ /**
+ * Test get_feedback with invalid requests.
+ *
+ * @covers ::get_feedback
+ * @dataProvider get_feedback_invalid_request_provider
+ * @param string $loggeduserrole The role of the logged user.
+ * @param bool $feedbacknotincourse Whether to request a feedback for a grade item which is not a part of the course.
+ * @param array $expectedexception The expected exception.
+ * @return void
+ */
+ public function test_get_feedback_invalid_request(string $loggeduserrole, bool $feedbacknotincourse,
+ array $expectedexception = []) {
+
+ $this->resetAfterTest(true);
+ // Create a course with a user and a grade item.
+ $course = $this->getDataGenerator()->create_course();
+ $user = $this->getDataGenerator()->create_user();
+ $this->getDataGenerator()->enrol_user($user->id, $course->id);
+ $gradeitem = $this->getDataGenerator()->create_grade_item(['courseid' => $course->id]);
+ // Add feedback for the grade item in course.
+ $gradegradedata = [
+ 'itemid' => $gradeitem->id,
+ 'userid' => $user->id,
+ 'feedback' => 'Test feedback',
+ ];
+
+ $this->getDataGenerator()->create_grade_grade($gradegradedata);
+ // Set the current user as specified.
+ if ($loggeduserrole === 'user') {
+ $this->setUser($user);
+ } else if ($loggeduserrole === 'guest') {
+ $this->setGuestUser();
+ } else {
+ $this->setAdminUser();
+ }
+
+ if ($feedbacknotincourse) { // Create a new course which will be later used in the feedback request call.
+ $course = $this->getDataGenerator()->create_course();
+ }
+
+ $this->expectException($expectedexception['exceptionclass']);
+
+ if (!empty($expectedexception['exceptionmessage'])) {
+ $this->expectExceptionMessage($expectedexception['exceptionmessage']);
+ }
+
+ get_feedback::execute($course->id, $user->id, $gradeitem->id);
+ }
+
+ /**
+ * Data provider for test_get_feedback_invalid_request().
+ *
+ * @return array
+ */
+ public function get_feedback_invalid_request_provider(): array {
+ return [
+ 'Logged user does not have permissions to view feedback.' => [
+ 'user',
+ false,
+ ['exceptionclass' => \required_capability_exception::class]
+ ],
+ 'Guest user cannot view feedback.' => [
+ 'guest',
+ false,
+ ['exceptionclass' => \require_login_exception::class]
+ ],
+ 'Request feedback for a grade item which is not a part of the course.' => [
+ 'admin',
+ true,
+ [
+ 'exceptionclass' => \invalid_parameter_exception::class,
+ 'exceptionmessage' => 'Course ID and item ID mismatch',
+ ]
+ ]
+ ];
+ }
+}
diff --git a/lang/en/grades.php b/lang/en/grades.php
index dce54c4f27e..52931db6a2a 100644
--- a/lang/en/grades.php
+++ b/lang/en/grades.php
@@ -232,6 +232,7 @@ $string['feedbackedit'] = 'Edit feedback';
$string['feedbackfiles'] = 'Feedback files';
$string['feedbackforgradeitems'] = 'Feedback for {$a}';
$string['feedbackhistoryfiles'] = 'Feedback history files';
+$string['feedbackprovided'] = 'Feedback provided';
$string['feedbacks'] = 'Feedbacks';
$string['feedbacksaved'] = 'Feedback saved';
$string['feedbackview'] = 'View feedback';
@@ -865,6 +866,7 @@ $string['verbosescales'] = 'Verbose scales';
$string['verbosescales_help'] = 'A verbose scale uses words rather than numbers. Set to \'Yes\' if both numerical and verbose scales are to be imported. Set to \'No\' if only numerical scales are to be imported.';
$string['viewas'] = 'View report as';
$string['viewbygroup'] = 'Group';
+$string['viewfeedback'] = 'View feedback';
$string['viewgrades'] = 'View grades';
$string['weight'] = 'weight';
$string['weight_help'] = 'A value used to determine the relative value of multiple grade items in a category or course.';
diff --git a/lib/classes/output/icon_system_fontawesome.php b/lib/classes/output/icon_system_fontawesome.php
index 7e42778d0f3..5fc08899f8d 100644
--- a/lib/classes/output/icon_system_fontawesome.php
+++ b/lib/classes/output/icon_system_fontawesome.php
@@ -195,6 +195,7 @@ class icon_system_fontawesome extends icon_system_font {
'theme:fp/view_tree_active' => 'fa-folder',
'core:i/addblock' => 'fa-plus-square',
'core:i/assignroles' => 'fa-user-plus',
+ 'core:i/asterisk' => 'fa-asterisk',
'core:i/backup' => 'fa-file-zip-o',
'core:i/badge' => 'fa-shield',
'core:i/breadcrumbdivider' => 'fa-angle-right',
diff --git a/lib/db/services.php b/lib/db/services.php
index be5f79db0e4..d9a020b6858 100644
--- a/lib/db/services.php
+++ b/lib/db/services.php
@@ -995,6 +995,12 @@ $functions = array(
'ajax' => true,
'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE],
],
+ 'core_grades_get_feedback' => [
+ 'classname' => 'core_grades\external\get_feedback',
+ 'description' => 'Get the feedback data for a grade item',
+ 'type' => 'read',
+ 'ajax' => true,
+ ],
'core_grading_get_definitions' => array(
'classname' => 'core_grading_external',
'methodname' => 'get_definitions',
diff --git a/pix/i/asterisk.png b/pix/i/asterisk.png
new file mode 100644
index 00000000000..51f016e4f31
Binary files /dev/null and b/pix/i/asterisk.png differ
diff --git a/pix/i/asterisk.svg b/pix/i/asterisk.svg
new file mode 100644
index 00000000000..7f99cdcdc65
--- /dev/null
+++ b/pix/i/asterisk.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file