MDL-66700 gradingform_guide: Support new grading panel
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 1 Oct 2019 07:28:24 +0000 (15:28 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Wed, 30 Oct 2019 02:23:41 +0000 (10:23 +0800)
Part of MDL-66074

22 files changed:
grade/grading/form/guide/amd/build/grades/grader/gradingpanel.min.js [new file with mode: 0644]
grade/grading/form/guide/amd/build/grades/grader/gradingpanel.min.js.map [new file with mode: 0644]
grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments.min.js [new file with mode: 0644]
grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments.min.js.map [new file with mode: 0644]
grade/grading/form/guide/amd/src/grades/grader/gradingpanel.js [new file with mode: 0644]
grade/grading/form/guide/amd/src/grades/grader/gradingpanel/comments.js [new file with mode: 0644]
grade/grading/form/guide/classes/grades/grader/gradingpanel/external/fetch.php [new file with mode: 0644]
grade/grading/form/guide/classes/grades/grader/gradingpanel/external/store.php [new file with mode: 0644]
grade/grading/form/guide/db/services.php [new file with mode: 0644]
grade/grading/form/guide/lang/en/gradingform_guide.php
grade/grading/form/guide/lib.php
grade/grading/form/guide/pix/info.png [new file with mode: 0644]
grade/grading/form/guide/pix/info.svg [new file with mode: 0644]
grade/grading/form/guide/pix/plus.png [new file with mode: 0644]
grade/grading/form/guide/pix/plus.svg [new file with mode: 0644]
grade/grading/form/guide/styles.css
grade/grading/form/guide/styles.scss [new file with mode: 0644]
grade/grading/form/guide/templates/grades/grader/gradingpanel.mustache [new file with mode: 0644]
grade/grading/form/guide/tests/generator/lib.php
grade/grading/form/guide/tests/grades_grader_gradingpanel_guide_external_fetch_test.php [new file with mode: 0644]
grade/grading/form/guide/tests/grades_grader_gradingpanel_guide_external_store_test.php [new file with mode: 0644]
grade/grading/form/guide/version.php

diff --git a/grade/grading/form/guide/amd/build/grades/grader/gradingpanel.min.js b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel.min.js
new file mode 100644 (file)
index 0000000..89c26ab
Binary files /dev/null and b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel.min.js differ
diff --git a/grade/grading/form/guide/amd/build/grades/grader/gradingpanel.min.js.map b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel.min.js.map
new file mode 100644 (file)
index 0000000..0d531ed
Binary files /dev/null and b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel.min.js.map differ
diff --git a/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments.min.js b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments.min.js
new file mode 100644 (file)
index 0000000..8939228
Binary files /dev/null and b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments.min.js differ
diff --git a/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments.min.js.map b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments.min.js.map
new file mode 100644 (file)
index 0000000..8d6f2a9
Binary files /dev/null and b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments.min.js.map differ
diff --git a/grade/grading/form/guide/amd/src/grades/grader/gradingpanel.js b/grade/grading/form/guide/amd/src/grades/grader/gradingpanel.js
new file mode 100644 (file)
index 0000000..a0324c4
--- /dev/null
@@ -0,0 +1,56 @@
+// 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_guide.
+ *
+ * @module     gradingform_guide/grades/grader/gradingpanel
+ * @package    gradingform_guide
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {call as fetchMany} from 'core/ajax';
+
+// 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_guide_grader_gradingpanel_fetch`,
+        args: {
+            component,
+            contextid,
+            itemname,
+            gradeduserid,
+        },
+    }])[0];
+};
+
+
+export const storeCurrentGrade = (component, contextid, itemname, gradeduserid, rootNode) => {
+    const form = rootNode.querySelector('form');
+
+    return fetchMany([{
+        methodname: `gradingform_guide_grader_gradingpanel_store`,
+        args: {
+            component,
+            contextid,
+            itemname,
+            gradeduserid,
+            formdata: jQuery(form).serialize(),
+        },
+    }])[0];
+};
diff --git a/grade/grading/form/guide/amd/src/grades/grader/gradingpanel/comments.js b/grade/grading/form/guide/amd/src/grades/grader/gradingpanel/comments.js
new file mode 100644 (file)
index 0000000..8e69154
--- /dev/null
@@ -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/>.
+
+/**
+ * Grading panel frequently used comments selector.
+ *
+ * @module     gradingform_guide/grades/grader/gradingpanel/comments
+ * @package    gradingform_guide
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+export const init = (rootId) => {
+    const rootNode = document.querySelector(`#${rootId}`);
+
+    rootNode.addEventListener('click', (e) => {
+        if (!e.target.matches('[data-gradingform_guide-role="frequent-comment"]')) {
+            return;
+        }
+
+        e.preventDefault();
+
+        const clicked = e.target.closest('[data-gradingform_guide-role="frequent-comment"]');
+        const criterion = clicked.closest('[data-gradingform-guide-role="criterion"]');
+        const remark = criterion.querySelector('[data-gradingform-guide-role="remark"]');
+
+        if (!remark) {
+            return;
+        }
+
+        if (remark.value.trim()) {
+            remark.value += `\n${clicked.innerHTML}`;
+        } else {
+            remark.value += clicked.innerHTML;
+        }
+    });
+};
diff --git a/grade/grading/form/guide/classes/grades/grader/gradingpanel/external/fetch.php b/grade/grading/form/guide/classes/grades/grader/gradingpanel/external/fetch.php
new file mode 100644 (file)
index 0000000..3f9c810
--- /dev/null
@@ -0,0 +1,283 @@
+<?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 marking guide for the grading panel.
+ *
+ * @package    gradingform_guide
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_guide\grades\grader\gradingpanel\external;
+
+use coding_exception;
+use context;
+use core_user;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_format_value;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use stdClass;
+
+/**
+ * Web services relating to fetching of a marking guide for the grading panel.
+ *
+ * @package    gradingform_guide
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @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 ('guide' !== $gradeitem->get_advanced_grading_method()) {
+            throw new moodle_exception(
+                "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a marking guide"
+            );
+        }
+
+        // Fetch the actual data.
+        $gradeduser = core_user::get_user($gradeduserid);
+
+        return self::get_fetch_data($gradeitem, $gradeduser);
+    }
+
+    /**
+     * Get the data to be fetched.
+     *
+     * @param component_gradeitem $gradeitem
+     * @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_guide_filling();
+        $context = $controller->get_context();
+        $definitionid = (int) $definition->id;
+
+        $criterion = [];
+        if ($definition->guide_criteria) {
+            $criterion = array_map(function($criterion) use ($definitionid, $fillings, $context) {
+                $result = [
+                    'id' => $criterion['id'],
+                    'name' => $criterion['shortname'],
+                    'maxscore' => $criterion['maxscore'],
+                    'description' => self::get_formatted_text(
+                        $context,
+                        $definitionid,
+                        'description',
+                        $criterion['description'],
+                        (int) $criterion['descriptionformat']
+                    ),
+                    'descriptionmarkers' => self::get_formatted_text(
+                        $context,
+                        $definitionid,
+                        'descriptionmarkers',
+                        $criterion['descriptionmarkers'],
+                        (int) $criterion['descriptionmarkersformat']
+                    ),
+                    'score' => null,
+                    'remark' => null,
+                ];
+
+                if (array_key_exists($criterion['id'], $fillings['criteria'])) {
+                    $filling = $fillings['criteria'][$criterion['id']];
+
+                    $result['score'] = $filling['score'];
+                    $result['remark'] = self::get_formatted_text(
+                        $context,
+                        $definitionid,
+                        'remark',
+                        $filling['remark'],
+                        (int) $filling['remarkformat']
+                    );
+                }
+
+                return $result;
+            }, $definition->guide_criteria);
+        }
+
+        $comments = [];
+        if ($definition->guide_comments) {
+            $comments = array_map(function($comment) use ($definitionid, $context) {
+                return [
+                    'id' => $comment['id'],
+                    'sortorder' => $comment['sortorder'],
+                    'description' => self::get_formatted_text(
+                        $context,
+                        $definitionid,
+                        'description',
+                        $comment['description'],
+                        (int) $comment['descriptionformat']
+                    ),
+                ];
+            }, $definition->guide_comments);
+        }
+
+        return [
+            'templatename' => 'gradingform_guide/grades/grader/gradingpanel',
+            'grade' => [
+                'instanceid' => $instance->get_id(),
+                'criterion' => $criterion,
+                'hascomments' => !empty($comments),
+                'comments' => $comments,
+                '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'),
+                'criterion' => new external_multiple_structure(
+                    new external_single_structure([
+                        'id' => new external_value(PARAM_INT, 'The id of the criterion'),
+                        'name' => new external_value(PARAM_RAW, 'The name of the criterion'),
+                        'maxscore' => new external_value(PARAM_FLOAT, 'The maximum score for this criterion'),
+                        'description' => new external_value(PARAM_RAW, 'The description of the criterion'),
+                        'descriptionmarkers' => new external_value(PARAM_RAW, 'The description of the criterion for markers'),
+                        'score' => new external_value(PARAM_FLOAT, 'The current score for user being assessed', VALUE_OPTIONAL),
+                        'remark' => new external_value(PARAM_RAW, 'Any remarks for this criterion for the user being assessed', VALUE_OPTIONAL),
+                    ]),
+                    'The criterion by which this item will be graded'
+                ),
+                'hascomments' => new external_value(PARAM_BOOL, 'Whether there are any frequently-used comments'),
+                'comments' => new external_multiple_structure(
+                    new external_single_structure([
+                        'id' => new external_value(PARAM_INT, 'Comment id'),
+                        'sortorder' => new external_value(PARAM_INT, 'The sortorder of this comment'),
+                        'description' => new external_value(PARAM_RAW, 'The comment value'),
+                    ]),
+                    'Frequently used comments'
+                ),
+                '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;
+    }
+}
diff --git a/grade/grading/form/guide/classes/grades/grader/gradingpanel/external/store.php b/grade/grading/form/guide/classes/grades/grader/gradingpanel/external/store.php
new file mode 100644 (file)
index 0000000..c9fdaae
--- /dev/null
@@ -0,0 +1,160 @@
+<?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 marking guide for the grading panel.
+ *
+ * @package    gradingform_guide
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_guide\grades\grader\gradingpanel\external;
+
+use coding_exception;
+use context;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use core_user;
+use external_api;
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use moodle_exception;
+
+/**
+ * Web services relating to storing of a marking guide for the grading panel.
+ *
+ * @package    gradingform_guide
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @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
+     * @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 ('guide' !== $gradeitem->get_advanced_grading_method()) {
+            throw new moodle_exception(
+                "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a marking guide"
+            );
+        }
+
+        // Parse the serialised string into an object.
+        $data = [];
+        parse_str($formdata, $data);
+
+        // Grade.
+        $gradeitem->store_grade_from_formdata($gradeduser, $USER, (object) $data);
+
+        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();
+    }
+}
diff --git a/grade/grading/form/guide/db/services.php b/grade/grading/form/guide/db/services.php
new file mode 100644 (file)
index 0000000..24a947e
--- /dev/null
@@ -0,0 +1,42 @@
+<?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/>.
+
+/**
+ * External functions and service definitions for the Marking Guide advanced grading form.
+ *
+ * @package    mod_forum
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$functions = [
+    'gradingform_guide_grader_gradingpanel_fetch' => [
+        'classname' => 'gradingform_guide\\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_guide_grader_gradingpanel_store' => [
+        'classname' => 'gradingform_guide\\grades\\grader\\gradingpanel\\external\\store',
+        'methodname' => 'execute',
+        'description' => 'Store the grading data for a user from the grader grading panel.',
+        'type' => 'write',
+        'ajax' => true,
+    ],
+];
index 9343b4a..6c9aebd 100644 (file)
@@ -25,6 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['addcomment'] = 'Add frequently used comment';
+$string['additionalcomments'] = 'Additional comments';
 $string['addcriterion'] = 'Add criterion';
 $string['alwaysshowdefinition'] = 'Show guide definition to students';
 $string['backtoediting'] = 'Back to editing';
@@ -73,6 +74,7 @@ $string['insertcomment'] = 'Insert frequently used comment';
 $string['maxscore'] = 'Maximum score';
 $string['name'] = 'Name';
 $string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
+$string['outof'] = 'Out of {$a}';
 $string['pluginname'] = 'Marking guide';
 $string['previewmarkingguide'] = 'Preview marking guide';
 $string['privacy:metadata:criterionid'] = 'An identifier to a criterion for advanced marking.';
index 4a4e002..ffec06c 100644 (file)
@@ -997,3 +997,15 @@ class gradingform_guide_instance extends gradingform_instance {
         return $html;
     }
 }
+
+/**
+ * Get the icon mapping for font-awesome.
+ *
+ * @return array
+ */
+function gradingform_guide_get_fontawesome_icon_map(): array {
+    return [
+        'gradingform_guide:info' => 'fa-info-circle',
+        'gradingform_guide:plus' => 'fa-plus',
+    ];
+}
diff --git a/grade/grading/form/guide/pix/info.png b/grade/grading/form/guide/pix/info.png
new file mode 100644 (file)
index 0000000..6f9aa77
Binary files /dev/null and b/grade/grading/form/guide/pix/info.png differ
diff --git a/grade/grading/form/guide/pix/info.svg b/grade/grading/form/guide/pix/info.svg
new file mode 100644 (file)
index 0000000..f14d549
--- /dev/null
@@ -0,0 +1,3 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 14c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm1.2-2c0 .5-.5 1-1 1h-.4c-.5 0-1-.5-1-1V7.4c0-.5.5-1 1-1h.5c.5 0 1 .5 1 1V12zm0-7.8c0 .7-.6 1.2-1.2 1.2-.7 0-1.2-.6-1.2-1.2C6.8 3.5 7.3 3 8 3s1.2.5 1.2 1.2z" fill="#999"/></svg>
\ No newline at end of file
diff --git a/grade/grading/form/guide/pix/plus.png b/grade/grading/form/guide/pix/plus.png
new file mode 100644 (file)
index 0000000..988d917
Binary files /dev/null and b/grade/grading/form/guide/pix/plus.png differ
diff --git a/grade/grading/form/guide/pix/plus.svg b/grade/grading/form/guide/pix/plus.svg
new file mode 100644 (file)
index 0000000..63ccf86
--- /dev/null
@@ -0,0 +1,3 @@
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+       <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M11 4.5H7.5V1c0-.5-.5-1-1-1h-1c-.5 0-1 .5-1 1v3.5H1c-.5 0-1 .5-1 1v1c0 .5.5 1 1 1h3.5V11c0 .5.5 1 1 1h1c.5 0 1-.5 1-1V7.5H11c.6 0 1-.5 1-1v-1c0-.5-.4-1-1-1z" fill="#999"/></svg>
\ No newline at end of file
index e3cd9dc..08cbb9d 100644 (file)
     max-height: 80vh;
     overflow-y: auto;
 }
+.gradingform_guide-frequent-comments {
+    position: absolute;
+    top: 7px;
+    right: 0px;
+}
diff --git a/grade/grading/form/guide/styles.scss b/grade/grading/form/guide/styles.scss
new file mode 100644 (file)
index 0000000..4898971
--- /dev/null
@@ -0,0 +1,5 @@
+.gradingform_guide-fac {
+    position: absolute;
+    right: -5px;
+    top: 5px;
+}
diff --git a/grade/grading/form/guide/templates/grades/grader/gradingpanel.mustache b/grade/grading/form/guide/templates/grades/grader/gradingpanel.mustache
new file mode 100644 (file)
index 0000000..2588328
--- /dev/null
@@ -0,0 +1,74 @@
+<form id="gradingform_guide-{{uniqid}}">
+  <input type="hidden" name="instanceid" value="{{instanceid}}">
+  {{#criterion}}
+    <div data-gradingform-guide-role="criterion">
+      <h5>
+        {{name}}
+        <a 
+          href="#gradingform_guide-{{uniqid}}-criteria-{{id}}-description" 
+          aria-controls="gradingform_guide-{{uniqid}}-criteria-{{id}}-description" 
+          aria-expanded="false" 
+          data-toggle="collapse" 
+          role="button" 
+          >
+            {{# pix }} info, gradingform_guide {{/ pix }}
+        </a>
+      </h5>
+      <div class="collapse" id="gradingform_guide-{{uniqid}}-criteria-{{id}}-description">
+          <div class="border p-3 mb-3 bg-white rounded">
+              {{{description}}}
+              {{#descriptionmarkers}}
+                  <hr>
+                  {{{descriptionmarkers}}}
+              {{/descriptionmarkers}}
+          </div>
+      </div>
+      <div class="form-group">
+        <label for="gradingform_guide-{{uniqid}}-criteria-{{id}}-score">{{#str}}outof, gradingform_guide, {{maxscore}}{{/str}}</label>
+        <input class="form-control" type="number" name="advancedgrading[criteria][{{id}}][score]" value="{{score}}" 
+            id="gradingform_guide-{{uniqid}}-criteria-{{id}}-score" 
+            aria-describedby="gradingform_guide-{{uniqid}}-help-{{id}}-score">
+        <small id="gradingform_guide-{{uniqid}}-help-{{id}}-score" class="sr-only">{{#str}}grade_help, gradingform_guide{{/str}}</small>
+      </div>
+      <div class="form-group ">
+        <label for="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark">{{#str}}additionalcomments, gradingform_guide{{/str}}</label>
+        <div class="input-group mb-3 form-inset form-inset-right">
+          <textarea class="form-control" type="text" name="advancedgrading[criteria][{{id}}][remark]" 
+              id="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark" 
+              aria-describedby="gradingform_guide-{{uniqid}}-help-{{id}}-remark" 
+              data-gradingform-guide-role="remark" 
+              >{{remark}}</textarea>
+          {{#hascomments}}
+            <a 
+              class="form-inset-item" 
+              href="#gradingform_guide-{{uniqid}}-criteria-{{id}}-remark-frequent-comments" 
+              aria-controls="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark-frequent-comments" 
+              aria-expanded="false" 
+              data-toggle="collapse" 
+              role="button" 
+              >
+                {{#pix}}plus, gradingform_guide{{/pix}}
+            </a>
+          {{/hascomments}}
+        </div>
+        {{#hascomments}}
+          <div class="collapse" id="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark-frequent-comments">
+            <div data-gradingform_guide-frequent-comments="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark">
+              <div class="list-group">
+                {{#comments}}
+                  <button type="button" class="list-group-item list-group-item-action" data-gradingform_guide-role="frequent-comment">{{description}}</button>
+                {{/comments}}
+              </div>
+            </div>
+          </div>
+        {{/hascomments}}
+        <small id="gradingform_guide-{{uniqid}}-help-{{id}}-remark" class="sr-only">{{#str}}grade_help, gradingform_guide{{/str}}</small>
+      </div>
+    </div>
+  {{/criterion}}
+</form>
+{{#js}}
+require(['gradingform_guide/grades/grader/gradingpanel/comments'], function(Comments) {
+  Comments.init('gradingform_guide-{{uniqid}}');
+});
+{{/js}}
index faf0cb1..ffb7634 100644 (file)
@@ -173,10 +173,14 @@ class gradingform_guide_generator extends component_generator_base {
      * @param context_module $context
      * @return gradingform_guide_controller
      */
-    public function get_test_guide(context_module $context): gradingform_guide_controller {
+    public function get_test_guide(
+        context_module $context,
+        string $component = 'mod_assign',
+        string $areaname = 'submission'
+    ): gradingform_guide_controller {
         $generator = \testing_util::get_data_generator();
         $gradinggenerator = $generator->get_plugin_generator('core_grading');
-        $controller = $gradinggenerator->create_instance($context, 'mod_assign', 'submission', 'guide');
+        $controller = $gradinggenerator->create_instance($context, $component, $areaname, 'guide');
 
         $generator = \testing_util::get_data_generator();
         $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
diff --git a/grade/grading/form/guide/tests/grades_grader_gradingpanel_guide_external_fetch_test.php b/grade/grading/form/guide/tests/grades_grader_gradingpanel_guide_external_fetch_test.php
new file mode 100644 (file)
index 0000000..1f58bbd
--- /dev/null
@@ -0,0 +1,296 @@
+<?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 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_guide\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 Andrew Nicols <andrew@nicols.co.uk>
+ * @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 marking guide");
+        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_guide/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('criterion', $result['grade']);
+        $criteria = $result['grade']['criterion'];
+        $this->assertCount(count($definition->guide_criteria), $criteria);
+        foreach ($criteria as $criterion) {
+            $this->assertArrayHasKey('id', $criterion);
+            $criterionid = $criterion['id'];
+            $sourcecriterion = $definition->guide_criteria[$criterionid];
+
+            $this->assertArrayHasKey('name', $criterion);
+            $this->assertEquals($sourcecriterion['shortname'], $criterion['name']);
+
+            $this->assertArrayHasKey('maxscore', $criterion);
+            $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']);
+
+            $this->assertArrayHasKey('description', $criterion);
+            $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+            $this->assertArrayHasKey('descriptionmarkers', $criterion);
+            $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']);
+
+            $this->assertArrayHasKey('score', $criterion);
+            $this->assertEmpty($criterion['score']);
+
+            $this->assertArrayHasKey('remark', $criterion);
+            $this->assertEmpty($criterion['remark']);
+        }
+    }
+
+    /**
+     * 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();
+        $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
+
+        [
+            '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 = $guidegenerator->get_test_form_data($controller, (int) $student->id,
+            10, 'Propper good speling',
+            0, 'ASCII art is not a picture'
+        );
+
+        $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_guide/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('criterion', $result['grade']);
+        $criteria = $result['grade']['criterion'];
+        $this->assertCount(count($definition->guide_criteria), $criteria);
+        foreach ($criteria as $criterion) {
+            $this->assertArrayHasKey('id', $criterion);
+            $criterionid = $criterion['id'];
+            $sourcecriterion = $definition->guide_criteria[$criterionid];
+
+            $this->assertArrayHasKey('name', $criterion);
+            $this->assertEquals($sourcecriterion['shortname'], $criterion['name']);
+
+            $this->assertArrayHasKey('maxscore', $criterion);
+            $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']);
+
+            $this->assertArrayHasKey('description', $criterion);
+            $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+            $this->assertArrayHasKey('descriptionmarkers', $criterion);
+            $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']);
+
+            $this->assertArrayHasKey('score', $criterion);
+            $this->assertArrayHasKey('remark', $criterion);
+        }
+
+        $this->assertEquals(10, $criteria[0]['score']);
+        $this->assertEquals('Propper good speling', $criteria[0]['remark']);
+        $this->assertEquals(0, $criteria[1]['score']);
+        $this->assertEquals('ASCII art is not a picture', $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 marking guide.
+     *
+     * @return array
+     */
+    protected function get_test_data(): array {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $generator = \testing_util::get_data_generator();
+        $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
+
+        $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 = $guidegenerator->get_test_guide($forum->get_context(), 'forum', 'forum');
+        $definition = $controller->get_definition();
+
+        $DB->set_field('forum', 'grade_forum', count($definition->guide_criteria), ['id' => $forum->get_id()]);
+        return [
+            'forum' => $forum,
+            'controller' => $controller,
+            'definition' => $definition,
+            'student' => $student,
+            'teacher' => $teacher,
+        ];
+    }
+}
diff --git a/grade/grading/form/guide/tests/grades_grader_gradingpanel_guide_external_store_test.php b/grade/grading/form/guide/tests/grades_grader_gradingpanel_guide_external_store_test.php
new file mode 100644 (file)
index 0000000..bc46350
--- /dev/null
@@ -0,0 +1,249 @@
+<?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 Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_guide\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 Andrew Nicols <andrew@nicols.co.uk>
+ * @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 marking guide");
+        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();
+        $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
+
+        [
+            '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 = $guidegenerator->get_test_form_data($controller, (int) $student->id,
+            10, 'Propper good speling',
+            0, 'ASCII art is not a picture'
+        );
+
+        $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_guide/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('criterion', $result['grade']);
+        $criteria = $result['grade']['criterion'];
+        $this->assertCount(count($definition->guide_criteria), $criteria);
+        foreach ($criteria as $criterion) {
+            $this->assertArrayHasKey('id', $criterion);
+            $criterionid = $criterion['id'];
+            $sourcecriterion = $definition->guide_criteria[$criterionid];
+
+            $this->assertArrayHasKey('name', $criterion);
+            $this->assertEquals($sourcecriterion['shortname'], $criterion['name']);
+
+            $this->assertArrayHasKey('maxscore', $criterion);
+            $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']);
+
+            $this->assertArrayHasKey('description', $criterion);
+            $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+            $this->assertArrayHasKey('descriptionmarkers', $criterion);
+            $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']);
+
+            $this->assertArrayHasKey('score', $criterion);
+            $this->assertArrayHasKey('remark', $criterion);
+        }
+
+        $this->assertEquals(10, $criteria[0]['score']);
+        $this->assertEquals('Propper good speling', $criteria[0]['remark']);
+        $this->assertEquals(0, $criteria[1]['score']);
+        $this->assertEquals('ASCII art is not a picture', $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 marking guide.
+     *
+     * @return array
+     */
+    protected function get_test_data(): array {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $generator = \testing_util::get_data_generator();
+        $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
+
+        $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 = $guidegenerator->get_test_guide($forum->get_context(), 'forum', 'forum');
+        $definition = $controller->get_definition();
+
+        $DB->set_field('forum', 'grade_forum', count($definition->guide_criteria), ['id' => $forum->get_id()]);
+        return [
+            'forum' => $forum,
+            'controller' => $controller,
+            'definition' => $definition,
+            'student' => $student,
+            'teacher' => $teacher,
+        ];
+    }
+}
index 9b6c827..a774d44 100644 (file)
@@ -25,6 +25,6 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component  = 'gradingform_guide';
-$plugin->version    = 2019052000;
+$plugin->version    = 2019100300;
 $plugin->requires   = 2019051100;
-$plugin->maturity   = MATURITY_STABLE;
\ No newline at end of file
+$plugin->maturity   = MATURITY_STABLE;