--- /dev/null
+// 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/>.
+
+/**
+ * Grading panel for gradingform_rubric.
+ *
+ * @module gradingform_rubric/grades/grader/gradingpanel
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {call as fetchMany} from 'core/ajax';
+import {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise';
+
+
+// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
+import jQuery from 'jquery';
+
+export const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {
+ return fetchMany([{
+ methodname: `gradingform_rubric_grader_gradingpanel_fetch`,
+ args: {
+ component,
+ contextid,
+ itemname,
+ gradeduserid,
+ },
+ }])[0];
+};
+
+
+export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, rootNode) => {
+ const form = rootNode.querySelector('form');
+
+ return normaliseResult(await fetchMany([{
+ methodname: `gradingform_rubric_grader_gradingpanel_store`,
+ args: {
+ component,
+ contextid,
+ itemname,
+ gradeduserid,
+ formdata: jQuery(form).serialize(),
+ },
+ }])[0]);
+};
--- /dev/null
+<?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/>.
+
+/**
+ * Web services relating to fetching of a rubric for the grading panel.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_rubric\grades\grader\gradingpanel\external;
+
+use coding_exception;
+use context;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use stdClass;
+use moodle_exception;
+
+/**
+ * Web services relating to fetching of a rubric for the grading panel.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch extends external_api {
+
+ /**
+ * Describes the parameters for fetching the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @return array
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ if ('rubric' !== $gradeitem->get_advanced_grading_method()) {
+ throw new moodle_exception(
+ "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a rubric"
+ );
+ }
+
+ // Fetch the actual data.
+ $gradeduser = \core_user::get_user($gradeduserid);
+
+ return self::get_fetch_data($gradeitem, $gradeduser);
+ }
+
+ /**
+ * Get the data to be fetched.
+ *
+ * @param gradeitem $gradeitem
+ * @param stdClass $gradeduser
+ * @return array
+ */
+ public static function get_fetch_data(gradeitem $gradeitem, stdClass $gradeduser): array {
+ global $USER;
+
+ $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+ $instance = $gradeitem->get_advanced_grading_instance($USER, $grade);
+ $controller = $instance->get_controller();
+ $definition = $controller->get_definition();
+ $fillings = $instance->get_rubric_filling();
+ $context = $controller->get_context();
+ $definitionid = (int) $definition->id;
+
+ $teacherdescription = self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'description',
+ $definition->description,
+ (int) $definition->descriptionformat
+ );
+
+ $criterion = [];
+ if ($definition->rubric_criteria) {
+ $criterion = array_map(function($criterion) use ($definitionid, $fillings, $context) {
+ $result = [
+ 'id' => $criterion['id'],
+ 'description' => self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'description',
+ $criterion['description'],
+ (int) $criterion['descriptionformat']
+ ),
+ ];
+
+ $filling = [];
+ if (array_key_exists($criterion['id'], $fillings['criteria'])) {
+ $filling = $fillings['criteria'][$criterion['id']];
+ $result['remark'] = self::get_formatted_text($context,
+ $definitionid,
+ 'remark',
+ $filling['remark'],
+ (int) $filling['remarkformat']
+ );
+ }
+
+ $result['levels'] = array_map(function($level) use ($criterion, $filling, $context, $definitionid) {
+ $result = [
+ 'id' => $level['id'],
+ 'criterionid' => $criterion['id'],
+ 'score' => $level['score'],
+ 'definition' => self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'definition',
+ $level['definition'],
+ (int) $level['definitionformat']
+ ),
+ 'checked' => null,
+ ];
+
+ if (array_key_exists('levelid', $filling) && $filling['levelid'] == $level['id']) {
+ $result['checked'] = true;
+ }
+
+ return $result;
+ }, $criterion['levels']);
+
+ return $result;
+ }, $definition->rubric_criteria);
+ }
+
+ return [
+ 'templatename' => 'gradingform_rubric/grades/grader/gradingpanel',
+ 'grade' => [
+ 'instanceid' => $instance->get_id(),
+ 'criteria' => $criterion,
+ 'rubricmode' => 'evaluate editable',
+ 'teacherdescription' => $teacherdescription,
+ 'canedit' => false,
+ 'timecreated' => $grade->timecreated,
+ 'timemodified' => $grade->timemodified,
+ ],
+ 'warnings' => [],
+ ];
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return new external_single_structure([
+ 'templatename' => new external_value(PARAM_SAFEPATH, 'The template to use when rendering this data'),
+ 'grade' => new external_single_structure([
+ 'instanceid' => new external_value(PARAM_INT, 'The id of the current grading instance'),
+ 'rubricmode' => new external_value(PARAM_RAW, 'The mode i.e. evaluate editable'),
+ 'canedit' => new external_value(PARAM_BOOL, 'Can the user edit this'),
+ 'criteria' => new external_multiple_structure(
+ new external_single_structure([
+ 'id' => new external_value(PARAM_INT, 'ID of the Criteria'),
+ 'description' => new external_value(PARAM_RAW, 'Description of the Criteria'),
+ 'remark' => new external_value(PARAM_RAW, 'Any remarks for this criterion for the user being assessed', VALUE_OPTIONAL),
+ 'levels' => new external_multiple_structure(new external_single_structure([
+ 'id' => new external_value(PARAM_INT, 'ID of level'),
+ 'criterionid' => new external_value(PARAM_INT, 'ID of the criterion this matches to'),
+ 'score' => new external_value(PARAM_INT, 'What this level is worth'),
+ 'definition' => new external_value(PARAM_RAW, 'Definition of the level'),
+ 'checked' => new external_value(PARAM_BOOL, 'Selected flag'),
+ ])),
+ ])
+ ),
+ 'timecreated' => new external_value(PARAM_INT, 'The time that the grade was created'),
+ 'timemodified' => new external_value(PARAM_INT, 'The time that the grade was last updated'),
+ ]),
+ 'warnings' => new external_warnings(),
+ ]);
+ }
+
+ /**
+ * Get a formatted version of the remark/description/etc.
+ *
+ * @param context $context
+ * @param int $definitionid
+ * @param string $filearea The file area of the field
+ * @param string $text The text to be formatted
+ * @param int $format The input format of the string
+ * @return string
+ */
+ protected static function get_formatted_text(context $context, int $definitionid, string $filearea, string $text, int $format): string {
+ $formatoptions = [
+ 'noclean' => false,
+ 'trusted' => false,
+ 'filter' => true,
+ ];
+ [$newtext, ] = external_format_text($text, $format, $context, 'grading', $filearea, $definitionid, $formatoptions);
+ return $newtext;
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Web services relating to fetching of a rubric for the grading panel.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_rubric\grades\grader\gradingpanel\external;
+
+use coding_exception;
+use context;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use moodle_exception;
+
+/**
+ * Web services relating to storing of a rubric for the grading panel.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class store extends external_api {
+
+ /**
+ * Describes the parameters for storing the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ 'formdata' => new external_value(
+ PARAM_RAW,
+ 'The serialised form data representing the grade',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @param string $formdata
+ * @return array
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid, string $formdata): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ // Validate that this gradeitem is actually enabled.
+ if (!$gradeitem->is_grading_enabled()) {
+ throw new moodle_exception("Grading is not enabled for {$itemname} in this context");
+ }
+
+ // Fetch the record for the graded user.
+ $gradeduser = \core_user::get_user($gradeduserid);
+
+ // Require that this user can save grades.
+ $gradeitem->require_user_can_grade($gradeduser, $USER);
+
+ if ('rubric' !== $gradeitem->get_advanced_grading_method()) {
+ throw new moodle_exception(
+ "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a rubric"
+ );
+ }
+
+ // Parse the serialised string into an object.
+ $data = [];
+ parse_str($formdata, $data);
+
+ // Grade.
+ $gradeitem->store_grade_from_formdata($gradeduser, $USER, (object) $data);
+
+ // Fetch the updated grade back out.
+ $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+
+ return fetch::get_fetch_data($gradeitem, $gradeduser);
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return fetch::execute_returns();
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Rubric external functions and service definitions.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$functions = [
+ 'gradingform_rubric_grader_gradingpanel_fetch' => [
+ 'classname' => 'gradingform_rubric\\grades\\grader\\gradingpanel\\external\\fetch',
+ 'methodname' => 'execute',
+ 'description' => 'Fetch the data required to display the grader grading panel, ' .
+ 'creating the grade item if required',
+ 'type' => 'write',
+ 'ajax' => true,
+ ],
+ 'gradingform_rubric_grader_gradingpanel_store' => [
+ 'classname' => 'gradingform_rubric\\grades\\grader\\gradingpanel\\external\\store',
+ 'methodname' => 'execute',
+ 'description' => 'Store the grading data for a user from the grader grading panel.',
+ 'type' => 'write',
+ 'ajax' => true,
+ ],
+];
+
+
$string['name'] = 'Name';
$string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
$string['pluginname'] = 'Rubric';
+$string['pointsvalue'] = '{$a} points';
$string['previewrubric'] = 'Preview rubric';
$string['privacy:metadata:criterionid'] = 'An identifier for a specific criterion being graded.';
$string['privacy:metadata:fillingssummary'] = 'Stores information about the user\'s grade created by the rubric.';
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/grade/grading/form/lib.php');
+require_once($CFG->dirroot.'/lib/filelib.php');
/**
* This controller encapsulates the rubric grading logic
position: relative;
float: right;
}
+
+.gradingpanel-gradingform_rubric [aria-checked="true"] {
+ border: 1px solid black;
+}
--- /dev/null
+{{!
+ 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 gradingform_rubric/grades/grader/gradingpanel
+
+ Classes required for JS:
+ * TODO
+
+ Data attributes required for JS:
+ * TODO
+
+ Context variables required for this template:
+ * TODO
+
+ Example context (json):
+ {
+ }
+}}
+<form id="gradingform_rubric-{{uniqid}}">
+ <input type="hidden" name="instanceid" value="{{instanceid}}">
+ <div class="gradingform_rubric-description">{{{teacherdescription}}}</div>
+ <div id="rubric-advancedgrading-{{uniqid}}" class="criterion">
+ {{#criteria}}
+ <div class="d-block mb-2">
+ <h5 class="d-inline px-0 font-weight-bold mb-0">{{{description}}}</h5>
+ <button class="d-inline btn p-0 font-weight-bold mb-0 pull-right collapse"
+ type="button"
+ data-toggle="collapse"
+ data-target="#criteria-{{id}}"
+ aria-expanded="true"
+ aria-controls="criteria-{{id}}">
+ </button>
+ </div>
+ <div class="collapse show" id="criteria-{{id}}">
+ {{#levels}}
+ <div class="form-check">
+ <input class="form-check-input level"
+ type="radio"
+ name="advancedgrading[criteria][{{criterionid}}][levelid]"
+ id="advancedgrading-criteria-{{criterionid}}-levels-{{id}}-definition"
+ value="{{id}}"
+ {{#checked}}
+ aria-checked="true"
+ tabindex="0"
+ checked
+ {{/checked}}
+ {{^checked}}
+ aria-checked="false"
+ tabindex="-1"
+ {{/checked}}
+ >
+ <label class="w-100" for="advancedgrading-criteria-{{criterionid}}-levels-{{id}}-definition">
+ <label class="font-weight-bold">
+ {{{definition}}}
+ </label>
+ <label class="pull-right font-weight-bold">
+ {{#str}}pointsvalue, gradingform_rubric, {{score}}{{/str}}
+ </label>
+ </label>
+ </div>
+ {{/levels}}
+ <div class="form-group">
+ <label for="advancedgrading-criteria-{{id}}-remark">Additional feedback</label>
+ <textarea class="form-control" name="advancedgrading[criteria][{{id}}][remark]" id="advancedgrading-criteria-{{id}}-remark" cols="10" rows="5">{{{remark}}}</textarea>
+ </div>
+ </div>
+ {{/criteria}}
+ </div>
+</form>
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package core_grades
+ * @category test
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_rubric\grades\grader\gradingpanel\external;
+
+use advanced_testcase;
+use coding_exception;
+use core_grades\component_gradeitem;
+use external_api;
+use mod_forum\local\entities\forum as forum_entity;
+use moodle_exception;
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package core_grades
+ * @category test
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch_test extends advanced_testcase {
+
+ public static function setupBeforeClass(): void {
+ global $CFG;
+ require_once("{$CFG->libdir}/externallib.php");
+ }
+
+ /**
+ * Ensure that an execute with an invalid component is rejected.
+ */
+ public function test_execute_invalid_component(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component");
+ fetch::execute('mod_invalid', 1, 'foo', 2);
+ }
+
+ /**
+ * Ensure that an execute with an invalid itemname on a valid component is rejected.
+ */
+ public function test_execute_invalid_itemname(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component");
+ fetch::execute('mod_forum', 1, 'foo', 2);
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_incorrect_type(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance([
+ // Negative numbers mean a scale.
+ 'grade_forum' => 5,
+ ]);
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ $this->expectExceptionMessage("not configured for advanced grading with a rubric");
+ fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_fetch_empty(): void {
+ $this->resetAfterTest();
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ $result = external_api::clean_returnvalue(fetch::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criteria', $result['grade']);
+ $criteria = $result['grade']['criteria'];
+ $this->assertCount(count($definition->rubric_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->rubric_criteria[$criterionid];
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('levels', $criterion);
+
+ $levels = $criterion['levels'];
+ foreach ($levels as $level) {
+ $levelid = $level['id'];
+ $sourcelevel = $sourcecriterion['levels'][$levelid];
+
+ $this->assertArrayHasKey('criterionid', $level);
+ $this->assertEquals($criterionid, $level['criterionid']);
+
+ $this->assertArrayHasKey('checked', $level);
+
+ $this->assertArrayHasKey('definition', $level);
+ $this->assertEquals($sourcelevel['definition'], $level['definition']);
+
+ $this->assertArrayHasKey('score', $level);
+ $this->assertEquals($sourcelevel['score'], $level['score']);
+ }
+ }
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_fetch_graded(): void {
+ $this->resetAfterTest();
+ $generator = \testing_util::get_data_generator();
+ $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+ $grade = $gradeitem->get_grade_for_user($student, $teacher);
+ $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade);
+
+ $submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $student->id,
+ 0, 'Too many mistakes. Please try again.',
+ 2, 'Great number of pictures. Well done.'
+ );
+
+ $gradeitem->store_grade_from_formdata($student, $teacher, (object) [
+ 'instanceid' => $instance->get_id(),
+ 'advancedgrading' => $submissiondata,
+ ]);
+
+ $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ $result = external_api::clean_returnvalue(fetch::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criteria', $result['grade']);
+ $criteria = $result['grade']['criteria'];
+ $this->assertCount(count($definition->rubric_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->rubric_criteria[$criterionid];
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('remark', $criterion);
+
+ $this->assertArrayHasKey('levels', $criterion);
+
+ $levels = $criterion['levels'];
+ foreach ($levels as $level) {
+ $levelid = $level['id'];
+ $sourcelevel = $sourcecriterion['levels'][$levelid];
+
+ $this->assertArrayHasKey('criterionid', $level);
+ $this->assertEquals($criterionid, $level['criterionid']);
+
+ $this->assertArrayHasKey('checked', $level);
+
+ $this->assertArrayHasKey('definition', $level);
+ $this->assertEquals($sourcelevel['definition'], $level['definition']);
+
+ $this->assertArrayHasKey('score', $level);
+ $this->assertEquals($sourcelevel['score'], $level['score']);
+ }
+
+ }
+
+ $this->assertEquals(1, $criteria[0]['levels'][0]['checked']);
+ $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']);
+ $this->assertEquals(1, $criteria[1]['levels'][2]['checked']);
+ $this->assertEquals('Great number of pictures. Well done.', $criteria[1]['remark']);
+ }
+
+ /**
+ * Get a forum instance.
+ *
+ * @param array $config
+ * @return forum_entity
+ */
+ protected function get_forum_instance(array $config = []): forum_entity {
+ $this->resetAfterTest();
+
+ $datagenerator = $this->getDataGenerator();
+ $course = $datagenerator->create_course();
+ $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
+
+ $vaultfactory = \mod_forum\local\container::get_vault_factory();
+ $vault = $vaultfactory->get_forum_vault();
+
+ return $vault->get_from_id((int) $forum->id);
+ }
+
+ /**
+ * Get test data for forums graded using a rubric.
+ *
+ * @return array
+ */
+ protected function get_test_data(): array {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $generator = \testing_util::get_data_generator();
+ $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ $this->setUser($teacher);
+ $controller = $rubricgenerator->get_test_rubric($forum->get_context(), 'forum', 'forum');
+ $definition = $controller->get_definition();
+
+ $DB->set_field('forum', 'grade_forum', count($definition->rubric_criteria), ['id' => $forum->get_id()]);
+ return [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ];
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package core_grades
+ * @category test
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_rubric\grades\grader\gradingpanel\external;
+
+use advanced_testcase;
+use coding_exception;
+use core_grades\component_gradeitem;
+use external_api;
+use mod_forum\local\entities\forum as forum_entity;
+use moodle_exception;
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package core_grades
+ * @category test
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class store_test extends advanced_testcase {
+
+ public static function setupBeforeClass(): void {
+ global $CFG;
+ require_once("{$CFG->libdir}/externallib.php");
+ }
+
+ /**
+ * Ensure that an execute with an invalid component is rejected.
+ */
+ public function test_execute_invalid_component(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component");
+ store::execute('mod_invalid', 1, 'foo', 2, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute with an invalid itemname on a valid component is rejected.
+ */
+ public function test_execute_invalid_itemname(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component");
+ store::execute('mod_forum', 1, 'foo', 2, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_incorrect_type(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance([
+ 'grade_forum' => 5,
+ ]);
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ //$this->expectExceptionMessage("not configured for advanced grading with a rubric");
+ store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_disabled(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ $this->expectExceptionMessage("Grading is not enabled");
+ store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_store_graded(): void {
+ $this->resetAfterTest();
+ $generator = \testing_util::get_data_generator();
+ $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+ $grade = $gradeitem->get_grade_for_user($student, $teacher);
+ $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade);
+
+ $submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $student->id,
+ 0, 'Too many mistakes. Please try again.',
+ 2, 'Great number of pictures. Well done.'
+ );
+
+ $formdata = http_build_query((object) [
+ 'instanceid' => $instance->get_id(),
+ 'advancedgrading' => $submissiondata,
+ ], '', '&');
+
+ $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, $formdata);
+ $result = external_api::clean_returnvalue(store::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criteria', $result['grade']);
+ $criteria = $result['grade']['criteria'];
+ $this->assertCount(count($definition->rubric_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->rubric_criteria[$criterionid];
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('remark', $criterion);
+
+ $this->assertArrayHasKey('levels', $criterion);
+
+ $levels = $criterion['levels'];
+ foreach ($levels as $level) {
+ $levelid = $level['id'];
+ $sourcelevel = $sourcecriterion['levels'][$levelid];
+
+ $this->assertArrayHasKey('criterionid', $level);
+ $this->assertEquals($criterionid, $level['criterionid']);
+
+ $this->assertArrayHasKey('checked', $level);
+
+ $this->assertArrayHasKey('definition', $level);
+ $this->assertEquals($sourcelevel['definition'], $level['definition']);
+
+ $this->assertArrayHasKey('score', $level);
+ $this->assertEquals($sourcelevel['score'], $level['score']);
+ }
+
+ }
+
+ $this->assertEquals(1, $criteria[0]['levels'][0]['checked']);
+ $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']);
+ $this->assertEquals(1, $criteria[1]['levels'][2]['checked']);
+ $this->assertEquals('Great number of pictures. Well done.', $criteria[1]['remark']);
+ }
+
+ /**
+ * Get a forum instance.
+ *
+ * @param array $config
+ * @return forum_entity
+ */
+ protected function get_forum_instance(array $config = []): forum_entity {
+ $this->resetAfterTest();
+
+ $datagenerator = $this->getDataGenerator();
+ $course = $datagenerator->create_course();
+ $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
+
+ $vaultfactory = \mod_forum\local\container::get_vault_factory();
+ $vault = $vaultfactory->get_forum_vault();
+
+ return $vault->get_from_id((int) $forum->id);
+ }
+
+ /**
+ * Get test data for forums graded using a rubric.
+ *
+ * @return array
+ */
+ protected function get_test_data(): array {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $generator = \testing_util::get_data_generator();
+ $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ $this->setUser($teacher);
+ $controller = $rubricgenerator->get_test_rubric($forum->get_context(), 'forum', 'forum');
+ $definition = $controller->get_definition();
+
+ $DB->set_field('forum', 'grade_forum', count($definition->rubric_criteria), ['id' => $forum->get_id()]);
+ return [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ];
+ }
+}
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'gradingform_rubric';
-$plugin->version = 2019052000;
+$plugin->version = 2019052006;
$plugin->requires = 2019051100;
return {
getContentForUserId: getContentForUserIdFunction(),
- getUsers: getUsersForCmidFunction()
+ getUsers: getUsersForCmidFunction(),
};
};
// Map postid => post.
const parentMap = new Map();
discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post));
-
const userPosts = discussion.posts.userposts.map(post => {
post.subject = null;
post.readonly = true;
<hr/>
</div>
<div class="grader-grading-panel-display col-sm-12">
- <h4 class="d-inline mb-0 fa fa-magic"></h4>
- <h4 class="d-inline mb-0 ">Grade:</h4>
+ <h4 class="d-inline mb-0 fa fa-star-o"></h4>
+ <h4 class="d-inline mb-0 ">{{#str}}grade, core_grades{{/str}}</h4>
<div data-region="grade" class="col-md-12 mt-3">
{{> mod_forum/local/grades/local/grader/grade_placeholder }}
</div>
}
}
+.criterion button.collapse[aria-expanded="true"]:before {
+ content: $fa-var-angle-down;
+ margin-right: 0;
+ @include fa-icon();
+ font-size: 16px;
+ width: 16px;
+}
+
+.criterion button.collapse[aria-expanded="false"]:before {
+ content: $fa-var-angle-up;
+ margin-right: 0;
+ @include fa-icon();
+ font-size: 16px;
+ width: 16px;
+}
+
// Set up grades layout.
.path-grade-edit-tree .setup-grades {
h4 {
margin-left: 5px;
margin-right: 12px; }
+.criterion button.collapse[aria-expanded="true"]:before {
+ content: "";
+ margin-right: 0;
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-size: 16px;
+ width: 16px; }
+
+.criterion button.collapse[aria-expanded="false"]:before {
+ content: "";
+ margin-right: 0;
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-size: 16px;
+ width: 16px; }
+
.path-grade-edit-tree .setup-grades h4 {
margin: 0; }
margin-left: 5px;
margin-right: 12px; }
+.criterion button.collapse[aria-expanded="true"]:before {
+ content: "";
+ margin-right: 0;
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-size: 16px;
+ width: 16px; }
+
+.criterion button.collapse[aria-expanded="false"]:before {
+ content: "";
+ margin-right: 0;
+ display: inline-block;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-size: 16px;
+ width: 16px; }
+
.path-grade-edit-tree .setup-grades h4 {
margin: 0; }