Merge branch 'MDL-67100-master' of git://github.com/sarjona/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Fri, 1 Nov 2019 03:07:40 +0000 (11:07 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Fri, 1 Nov 2019 03:07:40 +0000 (11:07 +0800)
238 files changed:
admin/tool/analytics/model.php
admin/tool/dataprivacy/tests/behat/dataexport.feature
analytics/classes/model.php
analytics/templates/insight_info_message_prediction.mustache
analytics/templates/notification_styles.mustache
backup/moodle2/backup_xml_transformer.class.php
backup/moodle2/tests/backup_xml_transformer_test.php
backup/util/helper/restore_structure_parser_processor.class.php
backup/util/helper/tests/restore_structure_parser_processor_test.php
course/amd/build/repository.min.js
course/amd/build/repository.min.js.map
course/amd/src/repository.js
course/externallib.php
course/modlib.php
course/moodleform_mod.php
course/tests/courselib_test.php
course/tests/externallib_test.php
filter/displayh5p/db/install.php [new file with mode: 0644]
filter/displayh5p/filter.php
filter/displayh5p/tests/filter_test.php
grade/amd/build/grades/grader/gradingpanel/normalise.min.js [new file with mode: 0644]
grade/amd/build/grades/grader/gradingpanel/normalise.min.js.map [new file with mode: 0644]
grade/amd/build/grades/grader/gradingpanel/point.min.js [new file with mode: 0644]
grade/amd/build/grades/grader/gradingpanel/point.min.js.map [new file with mode: 0644]
grade/amd/build/grades/grader/gradingpanel/repository.min.js [new file with mode: 0644]
grade/amd/build/grades/grader/gradingpanel/repository.min.js.map [new file with mode: 0644]
grade/amd/build/grades/grader/gradingpanel/scale.min.js [new file with mode: 0644]
grade/amd/build/grades/grader/gradingpanel/scale.min.js.map [new file with mode: 0644]
grade/amd/src/grades/grader/gradingpanel/normalise.js [new file with mode: 0644]
grade/amd/src/grades/grader/gradingpanel/point.js [new file with mode: 0644]
grade/amd/src/grades/grader/gradingpanel/repository.js [new file with mode: 0644]
grade/amd/src/grades/grader/gradingpanel/scale.js [new file with mode: 0644]
grade/classes/component_gradeitem.php [new file with mode: 0644]
grade/classes/component_gradeitems.php [new file with mode: 0644]
grade/classes/grades/grader/gradingpanel/point/external/fetch.php [new file with mode: 0644]
grade/classes/grades/grader/gradingpanel/point/external/store.php [new file with mode: 0644]
grade/classes/grades/grader/gradingpanel/scale/external/fetch.php [new file with mode: 0644]
grade/classes/grades/grader/gradingpanel/scale/external/store.php [new file with mode: 0644]
grade/classes/local/gradeitem/advancedgrading_mapping.php [new file with mode: 0644]
grade/classes/local/gradeitem/itemnumber_mapping.php [new file with mode: 0644]
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/build/grades/grader/gradingpanel/comments/selectors.min.js [new file with mode: 0644]
grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments/selectors.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/amd/src/grades/grader/gradingpanel/comments/selectors.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/coverage.php [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
grade/grading/form/lib.php
grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js [new file with mode: 0644]
grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js.map [new file with mode: 0644]
grade/grading/form/rubric/amd/src/grades/grader/gradingpanel.js [new file with mode: 0644]
grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/fetch.php [new file with mode: 0644]
grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/store.php [new file with mode: 0644]
grade/grading/form/rubric/db/services.php [new file with mode: 0644]
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/lib.php
grade/grading/form/rubric/styles.css
grade/grading/form/rubric/templates/grades/grader/gradingpanel.mustache [new file with mode: 0644]
grade/grading/form/rubric/tests/coverage.php [new file with mode: 0644]
grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_fetch_test.php [new file with mode: 0644]
grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_store_test.php [new file with mode: 0644]
grade/grading/form/rubric/version.php
grade/grading/lib.php
grade/grading/tests/coverage.php [new file with mode: 0644]
grade/templates/grades/grader/gradingpanel/point.mustache [new file with mode: 0644]
grade/templates/grades/grader/gradingpanel/scale.mustache [new file with mode: 0644]
grade/tests/behat/grade_to_pass.feature
grade/tests/component_gradeitems_test.php [new file with mode: 0644]
grade/tests/coverage.php [new file with mode: 0644]
grade/tests/grades_grader_gradingpanel_point_external_fetch_test.php [new file with mode: 0644]
grade/tests/grades_grader_gradingpanel_point_external_store_test.php [new file with mode: 0644]
grade/tests/grades_grader_gradingpanel_scale_external_fetch_test.php [new file with mode: 0644]
grade/tests/grades_grader_gradingpanel_scale_external_store_test.php [new file with mode: 0644]
h5p/classes/core.php
h5p/classes/factory.php
h5p/classes/framework.php
install/lang/hr_schools/langconfig.php
lang/en/grades.php
lang/en/grading.php
lang/en/moodle.php
lib/amd/build/loadingicon.min.js
lib/amd/build/loadingicon.min.js.map
lib/amd/build/templates.min.js
lib/amd/build/templates.min.js.map
lib/amd/build/toast.min.js [new file with mode: 0644]
lib/amd/build/toast.min.js.map [new file with mode: 0644]
lib/amd/src/loadingicon.js
lib/amd/src/templates.js
lib/amd/src/toast.js [new file with mode: 0644]
lib/db/services.php
lib/db/upgrade.php
lib/editor/atto/plugins/h5p/lang/en/atto_h5p.php
lib/editor/atto/plugins/h5p/lib.php
lib/editor/atto/plugins/h5p/pix/icon-white.png [new file with mode: 0644]
lib/editor/atto/plugins/h5p/pix/icon-white.svg [new file with mode: 0644]
lib/editor/atto/plugins/h5p/pix/icon.svg
lib/editor/atto/plugins/h5p/styles.css
lib/editor/atto/plugins/h5p/tests/behat/h5p.feature
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-debug.js
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button-min.js
lib/editor/atto/plugins/h5p/yui/build/moodle-atto_h5p-button/moodle-atto_h5p-button.js
lib/editor/atto/plugins/h5p/yui/src/button/js/button.js
lib/editor/atto/tests/fixtures/ipsums.h5p [new file with mode: 0644]
lib/form/editor.php
lib/form/modgrade.php
lib/form/tests/behat/modgrade_validation.feature
lib/gradelib.php
lib/templates/local/toast/message.mustache [new file with mode: 0644]
lib/templates/local/toast/wrapper.mustache [new file with mode: 0644]
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-debug.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue-min.js
lib/yui/build/moodle-core-notification-dialogue/moodle-core-notification-dialogue.js
lib/yui/src/notification/js/dialogue.js
media/player/videojs/styles.css
mod/assign/classes/grades/gradeitems.php [new file with mode: 0644]
mod/assign/lang/en/assign.php
mod/assign/lib.php
mod/assign/tests/locallib_test.php
mod/forum/amd/build/grades/expandconversation.min.js [new file with mode: 0644]
mod/forum/amd/build/grades/expandconversation.min.js.map [new file with mode: 0644]
mod/forum/amd/build/grades/grader.min.js [new file with mode: 0644]
mod/forum/amd/build/grades/grader.min.js.map [new file with mode: 0644]
mod/forum/amd/build/grades/grader/selectors.min.js [new file with mode: 0644]
mod/forum/amd/build/grades/grader/selectors.min.js.map [new file with mode: 0644]
mod/forum/amd/build/local/grades/grader.min.js [new file with mode: 0644]
mod/forum/amd/build/local/grades/grader.min.js.map [new file with mode: 0644]
mod/forum/amd/build/local/grades/local/grader/gradingpanel.min.js [new file with mode: 0644]
mod/forum/amd/build/local/grades/local/grader/gradingpanel.min.js.map [new file with mode: 0644]
mod/forum/amd/build/local/grades/local/grader/selectors.min.js [new file with mode: 0644]
mod/forum/amd/build/local/grades/local/grader/selectors.min.js.map [new file with mode: 0644]
mod/forum/amd/build/local/grades/local/grader/user_picker.min.js [new file with mode: 0644]
mod/forum/amd/build/local/grades/local/grader/user_picker.min.js.map [new file with mode: 0644]
mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js [new file with mode: 0644]
mod/forum/amd/build/local/grades/local/grader/user_picker/selectors.min.js.map [new file with mode: 0644]
mod/forum/amd/build/local/layout/fullscreen.min.js [new file with mode: 0644]
mod/forum/amd/build/local/layout/fullscreen.min.js.map [new file with mode: 0644]
mod/forum/amd/build/local/layouts.min.js [new file with mode: 0644]
mod/forum/amd/build/local/layouts.min.js.map [new file with mode: 0644]
mod/forum/amd/build/repository.min.js
mod/forum/amd/build/repository.min.js.map
mod/forum/amd/src/grades/expandconversation.js [new file with mode: 0644]
mod/forum/amd/src/grades/grader.js [new file with mode: 0644]
mod/forum/amd/src/grades/grader/selectors.js [new file with mode: 0644]
mod/forum/amd/src/local/grades/grader.js [new file with mode: 0644]
mod/forum/amd/src/local/grades/local/grader/gradingpanel.js [new file with mode: 0644]
mod/forum/amd/src/local/grades/local/grader/selectors.js [new file with mode: 0644]
mod/forum/amd/src/local/grades/local/grader/user_picker.js [new file with mode: 0644]
mod/forum/amd/src/local/grades/local/grader/user_picker/selectors.js [new file with mode: 0644]
mod/forum/amd/src/local/layout/fullscreen.js [new file with mode: 0644]
mod/forum/amd/src/local/layouts.js [new file with mode: 0644]
mod/forum/amd/src/repository.js
mod/forum/classes/grades/forum_gradeitem.php [new file with mode: 0644]
mod/forum/classes/grades/gradeitems.php [new file with mode: 0644]
mod/forum/classes/local/data_mappers/legacy/forum.php
mod/forum/classes/local/entities/forum.php
mod/forum/classes/local/exporters/forum.php
mod/forum/classes/local/factories/entity.php
mod/forum/classes/local/factories/renderer.php
mod/forum/classes/local/managers/capability.php
mod/forum/classes/local/renderers/discussion_list.php
mod/forum/classes/local/vaults/discussion.php
mod/forum/classes/local/vaults/post.php
mod/forum/classes/privacy/provider.php
mod/forum/db/access.php
mod/forum/db/install.xml
mod/forum/db/services.php
mod/forum/db/upgrade.php
mod/forum/deprecatedlib.php
mod/forum/externallib.php
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/mod_form.php
mod/forum/post.php
mod/forum/styles.css
mod/forum/templates/discussion_list.mustache
mod/forum/templates/grades/grade_button.mustache [new file with mode: 0644]
mod/forum/templates/grades/grader/discussion/post_modal.mustache [new file with mode: 0644]
mod/forum/templates/grades/grader/discussion/posts.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/grader.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/local/grader/content.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/local/grader/grade_placeholder.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/local/grader/grading.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/local/grader/gradingpanel/error.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/local/grader/navigation.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/local/grader/user_picker.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/local/grader/user_picker/user.mustache [new file with mode: 0644]
mod/forum/templates/local/grades/local/grader/user_picker_placeholder.mustache [new file with mode: 0644]
mod/forum/tests/behat/grade_forum.feature [new file with mode: 0644]
mod/forum/tests/coverage.php
mod/forum/tests/entities_forum_test.php
mod/forum/tests/externallib_test.php
mod/forum/tests/generator/lib.php
mod/forum/tests/grades_forum_gradeitem_test.php [new file with mode: 0644]
mod/forum/tests/grades_gradeitems_test.php [new file with mode: 0644]
mod/forum/tests/vaults_discussion_test.php
mod/forum/tests/vaults_post_test.php
mod/forum/version.php
mod/forum/view.php
mod/upgrade.txt
mod/workshop/classes/grades/gradeitems.php [new file with mode: 0644]
theme/boost/amd/build/aria.min.js
theme/boost/amd/build/aria.min.js.map
theme/boost/amd/src/aria.js
theme/boost/scss/moodle.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/forms.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/layout.scss [new file with mode: 0644]
theme/boost/scss/moodle/modules.scss
theme/boost/style/moodle.css
theme/boost/templates/columns1.mustache
theme/boost/templates/columns2.mustache
theme/boost/templates/embedded.mustache
theme/boost/templates/login.mustache
theme/boost/templates/maintenance.mustache
theme/boost/templates/secure.mustache
theme/classic/style/moodle.css
user/templates/upcoming_activities_due_insight_body.mustache
version.php

index 6b7511b..ab9f304 100644 (file)
@@ -159,6 +159,10 @@ switch ($action) {
                 $predictionsprocessor = false;
             }
 
+            if (!isset($data->contexts)) {
+                $data->contexts = null;
+            }
+
             $model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
             redirect($returnurl);
         }
index 948a728..1b63df7 100644 (file)
@@ -89,7 +89,7 @@ Feature: Data export from the privacy API
     And I reload the page
     And I should see "Download ready" in the "Export all of my personal data" "table_row"
     And I open the action menu in "Victim User 1" "table_row"
-    And following "Download" should download between "1" and "140000" bytes
+    And following "Download" should download between "1" and "142000" bytes
 
     And the following config values are set as admin:
       | privacyrequestexpiry | 1 | tool_dataprivacy |
index cc92516..9e484b1 100644 (file)
@@ -493,13 +493,15 @@ class model {
             $predictionsprocessor = $this->model->predictionsprocessor;
         }
 
-        if ($contextids !== false) {
+        if ($contextids === false) {
+            $contextsstr = $this->model->contextids;
+        } else if (!$contextids) {
+            $contextsstr = null;
+        } else {
             $contextsstr = json_encode($contextids);
 
             // Reset the internal cache.
             $this->contexts = null;
-        } else {
-            $contextsstr = $this->model->contextids;
         }
 
         if ($this->model->timesplitting !== $timesplittingid ||
index d6b2f09..6e66e8a 100644 (file)
@@ -38,9 +38,7 @@
 {{> core_analytics/notification_styles}}
 
 {{#body}}
-    <div>
-        {{{.}}}
-    </div>
+    {{{.}}}
 {{/body}}
 <br/>
 
index a942177..527db08 100644 (file)
     }
 }}
 
-<style>
+{{! The styles defined here will be included in the Moodle web UI and in emails. Emails do not include Moodle
+stylesheets so we want these styles to be applied to emails. However, they will also be included in the Moodle web UI.
+We use the not(.dir-ltr):not(.dir-rtl) so that this style is not applied to the Moodle UI.
+Note that gmail strips out HTML styles which selector includes the caracters (), so the font-family rule
+is not applied in gmail.}}
+<head><style>
 body:not(.dir-ltr):not(.dir-rtl) {
     font-family: 'Open Sans', sans-serif;
 }
-body:not(.dir-ltr):not(.dir-rtl) .btn-insight {
+.btn-insight {
     color: #007bff;
     background-color: transparent;
     display: inline-block;
@@ -47,10 +52,9 @@ body:not(.dir-ltr):not(.dir-rtl) .btn-insight {
     user-select: none;
     border: 1px solid #007bff;
     padding: .375rem .75rem;
-    font-size: .9375rem;
     line-height: 1.5;
     border-radius: 0;
     text-decoration: none;
     cursor: pointer;
 }
-</style>
+</style></head>
index eb5b176..e646165 100644 (file)
@@ -85,6 +85,10 @@ class backup_xml_transformer extends xml_contenttransformer {
         }
 
         $content = $this->process_filephp_links($content); // Replace all calls to file.php by $@FILEPHP@$ in a normalised way
+
+        // Replace all calls to h5p/embed.php by $@H5PEMBED@$.
+        $content = $this->process_h5pembedphp_links($content);
+
         $content = $this->encode_absolute_links($content); // Pass the content against all the found encoders
 
         return $content;
@@ -120,6 +124,25 @@ class backup_xml_transformer extends xml_contenttransformer {
         return $content;
     }
 
+    /**
+     * Replace all calls to /h5p/embed.php by $@H5PEMBED@$
+     * to allow restore the /h5p/embed.php url in
+     * other domains.
+     *
+     * @param  string $content
+     * @return string
+     */
+    private function process_h5pembedphp_links($content) {
+        global $CFG;
+
+        // No /h5p/embed.php, nothing to convert.
+        if (strpos($content, '/h5p/embed.php') === false) {
+            return $content;
+        }
+
+        return str_replace($CFG->wwwroot.'/h5p/embed.php', '$@H5PEMBED@$', $content);
+    }
+
     private function encode_absolute_links($content) {
         foreach ($this->absolute_links_encoders as $classname => $methodname) {
             $content = call_user_func(array($classname, $methodname), $content);
index 55af176..554aa0b 100644 (file)
@@ -76,6 +76,7 @@ class backup_xml_transformer_testcase extends advanced_testcase {
             array('http://test.test/file.php?file=%2F2', 'http://test.test/file.php?file=%2F2'),
             array('http://test.test/file.php?file=%2F1%2F1.jpg', '$@FILEPHP@$$@SLASH@$1.jpg'),
             array('http://test.test/file.php?file=%2F1%2F%2F1.jpg', '$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'),
+            array('http://test.test/h5p/embed.php?url=testurl', '$@H5PEMBED@$?url=testurl'),
         );
     }
 
index add8d85..9fc1f6e 100644 (file)
@@ -57,30 +57,39 @@ class restore_structure_parser_processor extends grouped_parser_processor {
             return $cdata;
         } else if (strlen($cdata) < 32) { // Impossible to have one link in 32cc
             return $cdata;                // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=)
-        } else if (strpos($cdata, '$@FILEPHP@$') === false) { // No $@FILEPHP@$, nothing to convert
-            return $cdata;
         }
 
-        if ($CFG->slasharguments) {
-            $slash = '/';
-            $forcedownload = '?forcedownload=1';
-        } else {
-            $slash = '%2F';
-            $forcedownload = '&amp;forcedownload=1';
-        }
+        if (strpos($cdata, '$@FILEPHP@$') !== false) {
+            // We need to convert $@FILEPHP@$.
+            if ($CFG->slasharguments) {
+                $slash = '/';
+                $forcedownload = '?forcedownload=1';
+            } else {
+                $slash = '%2F';
+                $forcedownload = '&amp;forcedownload=1';
+            }
+
+            // We have to remove trailing slashes, otherwise file URLs will be restored with an extra slash.
+            $basefileurl = rtrim(moodle_url::make_legacyfile_url($this->courseid, null)->out(true), $slash);
+            // Decode file.php calls.
+            $search = array ("$@FILEPHP@$");
+            $replace = array($basefileurl);
+            $result = str_replace($search, $replace, $cdata);
 
-        // We have to remove trailing slashes, otherwise file URLs will be restored with an extra slash.
-        $basefileurl = rtrim(moodle_url::make_legacyfile_url($this->courseid, null)->out(true), $slash);
-        // Decode file.php calls
-        $search = array ("$@FILEPHP@$");
-        $replace = array($basefileurl);
-        $result = str_replace($search, $replace, $cdata);
+            // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799.
+            $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
+            $replace = array($slash, $forcedownload);
 
-        // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799
-        $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
-        $replace = array($slash, $forcedownload);
+            $cdata = str_replace($search, $replace, $result);
+        }
+
+        if (strpos($cdata, '$@H5PEMBED@$') !== false) {
+            // We need to convert $@H5PEMBED@$.
+            // Decode embed.php calls.
+            $cdata = str_replace('$@H5PEMBED@$', $CFG->wwwroot.'/h5p/embed.php', $cdata);
+        }
 
-        return str_replace($search, $replace, $result);
+        return $cdata;
     }
 
     /**
index 069163a..ea5ab2c 100644 (file)
@@ -107,6 +107,11 @@ class restore_structure_parser_processor_test extends advanced_testcase {
                 "<a href='http://test.test/file.php?file=%2F1%2F1.jpg&amp;forcedownload=1'>Image</a>",
                 false
             ),
+            array(
+                "<iframe src='$@H5PEMBED@$?url=testurl'></iframe>",
+                "<iframe src='http://test.test/h5p/embed.php?url=testurl'></iframe>",
+                true
+            ),
         );
     }
 
index 3c301ff..2e4849b 100644 (file)
Binary files a/course/amd/build/repository.min.js and b/course/amd/build/repository.min.js differ
index 21f6694..1e49dc2 100644 (file)
Binary files a/course/amd/build/repository.min.js.map and b/course/amd/build/repository.min.js.map differ
index 6340eab..7e530a7 100644 (file)
@@ -94,8 +94,26 @@ define(['jquery', 'core/ajax'], function($, Ajax) {
         return Ajax.call([request])[0];
     };
 
+    /**
+     * Get the list of users enrolled in this cmid.
+     *
+     * @param {Number} cmid Course Module from which the users will be obtained
+     * @returns {Promise} Promise containing a list of users
+     */
+    var getEnrolledUsersFromCourseModuleID = function(cmid) {
+        var request = {
+            methodname: 'core_course_get_enrolled_users_by_cmid',
+            args: {
+                cmid: cmid,
+            },
+        };
+
+        return Ajax.call([request])[0];
+    };
+
     return {
         getEnrolledCoursesByTimelineClassification: getEnrolledCoursesByTimelineClassification,
-        getLastAccessedCourses: getLastAccessedCourses
+        getLastAccessedCourses: getLastAccessedCourses,
+        getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,
     };
 });
index bb2b08a..d3bab4d 100644 (file)
@@ -4053,4 +4053,82 @@ class core_course_external extends external_api {
     public static function get_recent_courses_returns() {
         return new external_multiple_structure(course_summary_exporter::get_read_structure(), 'Courses');
     }
+
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     */
+    public static function get_enrolled_users_by_cmid_parameters() {
+        return new external_function_parameters([
+            'cmid' => new external_value(PARAM_INT, 'id of the course module', VALUE_REQUIRED),
+        ]);
+    }
+
+    /**
+     * Get all users in a course for a given cmid.
+     *
+     * @param int $cmid Course Module id from which the users will be obtained
+     * @return array List of users
+     * @throws invalid_parameter_exception
+     */
+    public static function get_enrolled_users_by_cmid(int $cmid) {
+        $warnings = [];
+
+        [
+            'cmid' => $cmid,
+        ] = self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [
+                'cmid' => $cmid,
+        ]);
+
+        list($course, $cm) = get_course_and_cm_from_cmid($cmid);
+        $coursecontext = context_course::instance($course->id);
+        self::validate_context($coursecontext);
+
+        $enrolledusers = get_enrolled_users($coursecontext);
+
+        $users = array_map(function ($user) {
+            $user->fullname = fullname($user);
+            return $user;
+        }, $enrolledusers);
+        sort($users);
+
+        return [
+            'users' => $users,
+            'warnings' => $warnings,
+        ];
+    }
+
+    /**
+     * Returns description of method result value
+     *
+     * @return external_description
+     */
+    public static function get_enrolled_users_by_cmid_returns() {
+        return new external_single_structure([
+            'users' => new external_multiple_structure(self::user_description()),
+            'warnings' => new external_warnings(),
+        ]);
+    }
+
+    /**
+     * Create user return value description.
+     *
+     * @return external_description
+     */
+    public static function user_description() {
+        $userfields = array(
+            'id'    => new external_value(core_user::get_property_type('id'), 'ID of the user'),
+            'fullname' => new external_value(PARAM_TEXT, 'The full name of the user', VALUE_OPTIONAL),
+            'firstname'   => new external_value(
+                    core_user::get_property_type('firstname'),
+                        'The first name(s) of the user',
+                        VALUE_OPTIONAL),
+            'lastname'    => new external_value(
+                    core_user::get_property_type('lastname'),
+                        'The family name of the user',
+                        VALUE_OPTIONAL),
+        );
+        return new external_single_structure($userfields);
+    }
 }
index caf3868..181586d 100644 (file)
@@ -27,6 +27,8 @@
 
 defined('MOODLE_INTERNAL') || die;
 
+use \core_grades\component_gradeitems;
+
 require_once($CFG->dirroot.'/course/lib.php');
 
 /**
@@ -213,49 +215,63 @@ function edit_module_post_actions($moduleinfo, $course) {
     $hasgrades = plugin_supports('mod', $moduleinfo->modulename, FEATURE_GRADE_HAS_GRADE, false);
     $hasoutcomes = plugin_supports('mod', $moduleinfo->modulename, FEATURE_GRADE_OUTCOMES, true);
 
-    // Sync idnumber with grade_item.
-    if ($hasgrades && $grade_item = grade_item::fetch(array('itemtype'=>'mod', 'itemmodule'=>$moduleinfo->modulename,
-                 'iteminstance'=>$moduleinfo->instance, 'itemnumber'=>0, 'courseid'=>$course->id))) {
-        $gradeupdate = false;
-        if ($grade_item->idnumber != $moduleinfo->cmidnumber) {
-            $grade_item->idnumber = $moduleinfo->cmidnumber;
-            $gradeupdate = true;
-        }
-        if (isset($moduleinfo->gradepass) && $grade_item->gradepass != $moduleinfo->gradepass) {
-            $grade_item->gradepass = $moduleinfo->gradepass;
-            $gradeupdate = true;
-        }
-        if ($gradeupdate) {
-            $grade_item->update();
-        }
-    }
-
-    if ($hasgrades) {
-        $items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$moduleinfo->modulename,
-                                         'iteminstance'=>$moduleinfo->instance, 'courseid'=>$course->id));
-    } else {
-        $items = array();
-    }
+    $items = grade_item::fetch_all([
+        'itemtype' => 'mod',
+        'itemmodule' => $moduleinfo->modulename,
+        'iteminstance' => $moduleinfo->instance,
+        'courseid' => $course->id,
+    ]);
 
     // Create parent category if requested and move to correct parent category.
-    if ($items and isset($moduleinfo->gradecat)) {
-        if ($moduleinfo->gradecat == -1) {
-            $grade_category = new grade_category();
-            $grade_category->courseid = $course->id;
-            $grade_category->fullname = $moduleinfo->name;
-            $grade_category->insert();
-            if ($grade_item) {
-                $parent = $grade_item->get_parent_category();
-                $grade_category->set_parent($parent->id);
+    $component = "mod_{$moduleinfo->modulename}";
+    if ($items) {
+        foreach ($items as $item) {
+            $update = false;
+
+            // Sync idnumber with grade_item.
+            // Note: This only happens for itemnumber 0 at this time.
+            if ($item->itemnumber == 0 && ($item->idnumber != $moduleinfo->cmidnumber)) {
+                $item->idnumber = $moduleinfo->cmidnumber;
+                $update = true;
             }
-            $moduleinfo->gradecat = $grade_category->id;
-        }
 
-        foreach ($items as $itemid=>$unused) {
-            $items[$itemid]->set_parent($moduleinfo->gradecat);
-            if ($itemid == $grade_item->id) {
-                // Use updated grade_item.
-                $grade_item = $items[$itemid];
+            // Determine the grade category.
+            $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $item->itemnumber, 'gradecat');
+            if (property_exists($moduleinfo, $gradecatfieldname)) {
+                $gradecat = $moduleinfo->$gradecatfieldname;
+                if ($gradecat == -1) {
+                    $gradecategory = new grade_category();
+                    $gradecategory->courseid = $course->id;
+                    $gradecategory->fullname = $moduleinfo->name;
+                    $gradecategory->insert();
+
+                    $parent = $item->get_parent_category();
+                    $gradecategory->set_parent($parent->id);
+                    $gradecat = $gradecategory->id;
+                }
+
+                $oldgradecat = null;
+                if ($parent = $item->get_parent_category()) {
+                    $oldgradecat = $parent->id;
+                }
+                if ($oldgradecat != $gradecat) {
+                    $item->set_parent($gradecat);
+                    $update = true;
+                }
+            }
+
+            // Determine the gradepass.
+            $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $item->itemnumber, 'gradepass');
+            if (isset($moduleinfo->{$gradepassfieldname})) {
+                $gradepass = $moduleinfo->{$gradepassfieldname};
+                if (null !== $gradepass && $gradepass != $item->gradepass) {
+                    $item->gradepass = $gradepass;
+                    $update = true;
+                }
+            }
+
+            if ($update) {
+                $item->update();
             }
         }
     }
@@ -263,8 +279,6 @@ function edit_module_post_actions($moduleinfo, $course) {
     require_once($CFG->libdir.'/grade/grade_outcome.php');
     // Add outcomes if requested.
     if ($hasoutcomes && $outcomes = grade_outcome::fetch_all_available($course->id)) {
-        $grade_items = array();
-
         // Outcome grade_item.itemnumber start at 1000, there is nothing above outcomes.
         $max_itemnumber = 999;
         if ($items) {
@@ -279,7 +293,7 @@ function edit_module_post_actions($moduleinfo, $course) {
             $elname = 'outcome_'.$outcome->id;
 
             if (property_exists($moduleinfo, $elname) and $moduleinfo->$elname) {
-                // So we have a request for new outcome grade item?
+                // Check if this is a new outcome grade item.
                 if ($items) {
                     $outcomeexists = false;
                     foreach($items as $item) {
@@ -295,25 +309,25 @@ function edit_module_post_actions($moduleinfo, $course) {
 
                 $max_itemnumber++;
 
-                $outcome_item = new grade_item();
-                $outcome_item->courseid     = $course->id;
-                $outcome_item->itemtype     = 'mod';
-                $outcome_item->itemmodule   = $moduleinfo->modulename;
-                $outcome_item->iteminstance = $moduleinfo->instance;
-                $outcome_item->itemnumber   = $max_itemnumber;
-                $outcome_item->itemname     = $outcome->fullname;
-                $outcome_item->outcomeid    = $outcome->id;
-                $outcome_item->gradetype    = GRADE_TYPE_SCALE;
-                $outcome_item->scaleid      = $outcome->scaleid;
-                $outcome_item->insert();
-
-                // Move the new outcome into correct category and fix sortorder if needed.
-                if ($grade_item) {
-                    $outcome_item->set_parent($grade_item->categoryid);
-                    $outcome_item->move_after_sortorder($grade_item->sortorder);
+                $outcomeitem = new grade_item();
+                $outcomeitem->courseid     = $course->id;
+                $outcomeitem->itemtype     = 'mod';
+                $outcomeitem->itemmodule   = $moduleinfo->modulename;
+                $outcomeitem->iteminstance = $moduleinfo->instance;
+                $outcomeitem->itemnumber   = $max_itemnumber;
+                $outcomeitem->itemname     = $outcome->fullname;
+                $outcomeitem->outcomeid    = $outcome->id;
+                $outcomeitem->gradetype    = GRADE_TYPE_SCALE;
+                $outcomeitem->scaleid      = $outcome->scaleid;
+                $outcomeitem->insert();
 
+                if ($items) {
+                    // Move the new outcome into the same category and immediately after the first grade item.
+                    $item = reset($items);
+                    $outcomeitem->set_parent($item->categoryid);
+                    $outcomeitem->move_after_sortorder($item->sortorder);
                 } else if (isset($moduleinfo->gradecat)) {
-                    $outcome_item->set_parent($moduleinfo->gradecat);
+                    $outcomeitem->set_parent($moduleinfo->gradecat);
                 }
             }
         }
@@ -354,7 +368,6 @@ function edit_module_post_actions($moduleinfo, $course) {
     return $moduleinfo;
 }
 
-
 /**
  * Set module info default values for the unset module attributs.
  *
@@ -702,34 +715,43 @@ function get_moduleinfo_data($cm, $course) {
         }
     }
 
-    if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$data->modulename,
-                                             'iteminstance'=>$data->instance, 'courseid'=>$course->id))) {
+    $component = "mod_{$data->modulename}";
+    $items = grade_item::fetch_all([
+        'itemtype' => 'mod',
+        'itemmodule' => $data->modulename,
+        'iteminstance' => $data->instance,
+        'courseid' => $course->id,
+    ]);
+
+    if ($items) {
         // Add existing outcomes.
         foreach ($items as $item) {
             if (!empty($item->outcomeid)) {
                 $data->{'outcome_' . $item->outcomeid} = 1;
             } else if (isset($item->gradepass)) {
-                $decimalpoints = $item->get_decimals();
-                $data->gradepass = format_float($item->gradepass, $decimalpoints);
+                $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $item->itemnumber, 'gradepass');
+                $data->{$gradepassfieldname} = format_float($item->gradepass, $item->get_decimals());
             }
+
         }
 
         // set category if present
-        $gradecat = false;
+        $gradecat = [];
         foreach ($items as $item) {
-            if ($gradecat === false) {
-                $gradecat = $item->categoryid;
-                continue;
+            if (!isset($gradecat[$item->itemnumber])) {
+                $gradecat[$item->itemnumber] = $item->categoryid;
             }
-            if ($gradecat != $item->categoryid) {
-                //mixed categories
-                $gradecat = false;
-                break;
+            if ($gradecat[$item->itemnumber] != $item->categoryid) {
+                // Mixed categories.
+                $gradecat[$item->itemnumber] = false;
             }
         }
-        if ($gradecat !== false) {
-            // do not set if mixed categories present
-            $data->gradecat = $gradecat;
+        foreach ($gradecat as $itemnumber => $cat) {
+            if ($cat !== false) {
+                $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradecat');
+                // Do not set if mixed categories present.
+                $data->{$gradecatfieldname} = $cat;
+            }
         }
     }
     return array($cm, $context, $module, $data, $cw);
index c977a14..ead3500 100644 (file)
@@ -1,12 +1,40 @@
 <?php
-require_once ($CFG->libdir.'/formslib.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/>.
+
+/**
+ * Moodleform.
+ *
+ * @package   core_course
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->libdir.'/formslib.php');
 require_once($CFG->libdir.'/completionlib.php');
 require_once($CFG->libdir.'/gradelib.php');
 require_once($CFG->libdir.'/plagiarismlib.php');
 
+use core_grades\component_gradeitems;
+
 /**
- * This class adds extra methods to form wrapper specific to be used for module
- * add / update forms mod/{modname}/mod_form.php replaced deprecated mod/{modname}/mod.html
+ * This class adds extra methods to form wrapper specific to be used for module add / update forms
+ * mod/{modname}/mod_form.php replaced deprecated mod/{modname}/mod.html Moodleform.
+ *
+ * @package   core_course
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
  */
 abstract class moodleform_mod extends moodleform {
     /** Current data */
@@ -68,6 +96,9 @@ abstract class moodleform_mod extends moodleform {
     /** @var object The course format of the current course. */
     protected $courseformat;
 
+    /** @var string Whether this is graded or rated. */
+    private $gradedorrated = null;
+
     public function __construct($current, $section, $cm, $course) {
         global $CFG;
 
@@ -215,18 +246,19 @@ abstract class moodleform_mod extends moodleform {
         if ($id = $mform->getElementValue('update')) {
             $modulename = $mform->getElementValue('modulename');
             $instance   = $mform->getElementValue('instance');
+            $component = "mod_{$modulename}";
 
             if ($this->_features->gradecat) {
-                $gradecat = false;
-                if (!empty($CFG->enableoutcomes) and $this->_features->outcomes) {
-                    $outcomes = grade_outcome::fetch_all_available($COURSE->id);
-                    if (!empty($outcomes)) {
-                        $gradecat = true;
-                    }
-                }
-
                 $hasgradeitems = false;
-                $items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename,'iteminstance'=>$instance, 'courseid'=>$COURSE->id));
+                $items = grade_item::fetch_all([
+                    'itemtype' => 'mod',
+                    'itemmodule' => $modulename,
+                    'iteminstance' => $instance,
+                    'courseid' => $COURSE->id,
+                ]);
+
+                $gradecategories = [];
+                $removecategories = [];
                 //will be no items if, for example, this activity supports ratings but rating aggregate type == no ratings
                 if (!empty($items)) {
                     foreach ($items as $item) {
@@ -241,32 +273,23 @@ abstract class moodleform_mod extends moodleform {
                     }
 
                     foreach ($items as $item) {
-                        if (is_bool($gradecat)) {
-                            $gradecat = $item->categoryid;
-                            continue;
-                        }
-                        if ($gradecat != $item->categoryid) {
-                            //mixed categories
-                            $gradecat = false;
-                            break;
+                        $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber(
+                            $component,
+                            $item->itemnumber,
+                            'gradecat'
+                        );
+
+                        if (!isset($gradecategories[$gradecatfieldname])) {
+                            $gradecategories[$gradecatfieldname] = $item->categoryid;
+                        } else if ($gradecategories[$gradecatfieldname] != $item->categoryid) {
+                            $removecategories[$gradecatfieldname] = true;
                         }
                     }
                 }
 
-                if (!$hasgradeitems && $mform->elementExists('gradepass')) {
-                    // Remove form element 'Grade to pass' since there are no grade items (when rating not selected).
-                    $mform->removeElement('gradepass');
-                }
-
-                if ($gradecat === false) {
-                    // items and outcomes in different categories - remove the option
-                    // TODO: add a "Mixed categories" text instead of removing elements with no explanation
-                    if ($mform->elementExists('gradecat')) {
-                        $mform->removeElement('gradecat');
-                        if ($this->_features->rating  && !$mform->elementExists('gradepass')) {
-                            //if supports ratings then the max grade dropdown wasnt added so the grade box can be removed entirely
-                            $mform->removeElement('modstandardgrade');
-                        }
+                foreach ($removecategories as $toremove) {
+                    if ($mform->elementExists($toremove)) {
+                        $mform->removeElement($toremove);
                     }
                 }
             }
@@ -274,13 +297,13 @@ abstract class moodleform_mod extends moodleform {
 
         if ($COURSE->groupmodeforce) {
             if ($mform->elementExists('groupmode')) {
-                $mform->hardFreeze('groupmode'); // groupmode can not be changed if forced from course settings
+                // The groupmode can not be changed if forced from course settings.
+                $mform->hardFreeze('groupmode');
             }
         }
 
-        // Don't disable/remove groupingid if it is currently set to something,
-        // otherwise you cannot turn it off at same time as turning off other
-        // option (MDL-30764)
+        // Don't disable/remove groupingid if it is currently set to something, otherwise you cannot turn it off at same
+        // time as turning off other option (MDL-30764).
         if (empty($this->_cm) || !$this->_cm->groupingid) {
             if ($mform->elementExists('groupmode') && empty($COURSE->groupmodeforce)) {
                 $mform->hideIf('groupingid', 'groupmode', 'eq', NOGROUPS);
@@ -386,36 +409,45 @@ abstract class moodleform_mod extends moodleform {
             }
         }
 
-        // Ratings: Don't let them select an aggregate type without selecting a scale.
-        // If the user has selected to use ratings but has not chosen a scale or set max points then the form is
-        // invalid. If ratings have been selected then the user must select either a scale or max points.
-        // This matches (horrible) logic in data_preprocessing.
-        if (isset($data['assessed']) && $data['assessed'] > 0 && empty($data['scale'])) {
-            $errors['assessed'] = get_string('scaleselectionrequired', 'rating');
-        }
-
-        // Check that the grade pass is a valid number.
-        $gradepassvalid = false;
-        if (isset($data['gradepass'])) {
-            if (unformat_float($data['gradepass'], true) === false) {
-                $errors['gradepass'] = get_string('err_numeric', 'form');
-            } else {
-                $gradepassvalid = true;
+        $component = "mod_{$this->_modname}";
+        $itemnames = component_gradeitems::get_itemname_mapping_for_component($component);
+        foreach ($itemnames as $itemnumber => $itemname) {
+            $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade');
+            $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradepass');
+            $assessedfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'assessed');
+            $scalefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'scale');
+
+            // Ratings: Don't let them select an aggregate type without selecting a scale.
+            // If the user has selected to use ratings but has not chosen a scale or set max points then the form is
+            // invalid. If ratings have been selected then the user must select either a scale or max points.
+            // This matches (horrible) logic in data_preprocessing.
+            if (isset($data[$assessedfieldname]) && $data[$assessedfieldname] > 0 && empty($data[$scalefieldname])) {
+                $errors[$assessedfieldname] = get_string('scaleselectionrequired', 'rating');
             }
-        }
 
-        // Grade to pass: ensure that the grade to pass is valid for points and scales.
-        // If we are working with a scale, convert into a positive number for validation.
-        if ($gradepassvalid && isset($data['gradepass']) && (!empty($data['grade']) || !empty($data['scale']))) {
-            $scale = !empty($data['grade']) ? $data['grade'] : $data['scale'];
-            if ($scale < 0) {
-                $scalevalues = $DB->get_record('scale', array('id' => -$scale));
-                $grade = count(explode(',', $scalevalues->scale));
-            } else {
-                $grade = $scale;
+            // Check that the grade pass is a valid number.
+            $gradepassvalid = false;
+            if (isset($data[$gradepassfieldname])) {
+                if (unformat_float($data[$gradepassfieldname], true) === false) {
+                    $errors[$gradepassfieldname] = get_string('err_numeric', 'form');
+                } else {
+                    $gradepassvalid = true;
+                }
             }
-            if (unformat_float($data['gradepass']) > $grade) {
-                $errors['gradepass'] = get_string('gradepassgreaterthangrade', 'grades', $grade);
+
+            // Grade to pass: ensure that the grade to pass is valid for points and scales.
+            // If we are working with a scale, convert into a positive number for validation.
+            if ($gradepassvalid && isset($data[$gradepassfieldname]) && (!empty($data[$gradefieldname]) || !empty($data[$scalefieldname]))) {
+                $scale = !empty($data[$gradefieldname]) ? $data[$gradefieldname] : $data[$scalefieldname];
+                if ($scale < 0) {
+                    $scalevalues = $DB->get_record('scale', array('id' => -$scale));
+                    $grade = count(explode(',', $scalevalues->scale));
+                } else {
+                    $grade = $scale;
+                }
+                if (unformat_float($data[$gradepassfieldname]) > $grade) {
+                    $errors[$gradepassfieldname] = get_string('gradepassgreaterthangrade', 'grades', $grade);
+                }
             }
         }
 
@@ -485,7 +517,7 @@ abstract class moodleform_mod extends moodleform {
     /**
      * Adds all the standard elements to a form to edit the settings for an activity module.
      */
-    function standard_coursemodule_elements(){
+    protected function standard_coursemodule_elements() {
         global $COURSE, $CFG, $DB;
         $mform =& $this->_form;
 
@@ -500,70 +532,10 @@ abstract class moodleform_mod extends moodleform {
             }
         }
 
-
         if ($this->_features->rating) {
-            require_once($CFG->dirroot.'/rating/lib.php');
-            $rm = new rating_manager();
-
-            $mform->addElement('header', 'modstandardratings', get_string('ratings', 'rating'));
-
-            $permission=CAP_ALLOW;
-            $rolenamestring = null;
-            $isupdate = false;
-            if (!empty($this->_cm)) {
-                $isupdate = true;
-                $context = context_module::instance($this->_cm->id);
-
-                $rolenames = get_role_names_with_caps_in_context($context, array('moodle/rating:rate', 'mod/'.$this->_cm->modname.':rate'));
-                $rolenamestring = implode(', ', $rolenames);
-            } else {
-                $rolenamestring = get_string('capabilitychecknotavailable','rating');
-            }
-            $mform->addElement('static', 'rolewarning', get_string('rolewarning','rating'), $rolenamestring);
-            $mform->addHelpButton('rolewarning', 'rolewarning', 'rating');
-
-            $mform->addElement('select', 'assessed', get_string('aggregatetype', 'rating') , $rm->get_aggregate_types());
-            $mform->setDefault('assessed', 0);
-            $mform->addHelpButton('assessed', 'aggregatetype', 'rating');
-
-            $gradeoptions = array('isupdate' => $isupdate,
-                                  'currentgrade' => false,
-                                  'hasgrades' => false,
-                                  'canrescale' => $this->_features->canrescale,
-                                  'useratings' => $this->_features->rating);
-            if ($isupdate) {
-                $gradeitem = grade_item::fetch(array('itemtype' => 'mod',
-                                                     'itemmodule' => $this->_cm->modname,
-                                                     'iteminstance' => $this->_cm->instance,
-                                                     'itemnumber' => 0,
-                                                     'courseid' => $COURSE->id));
-                if ($gradeitem) {
-                    $gradeoptions['currentgrade'] = $gradeitem->grademax;
-                    $gradeoptions['currentgradetype'] = $gradeitem->gradetype;
-                    $gradeoptions['currentscaleid'] = $gradeitem->scaleid;
-                    $gradeoptions['hasgrades'] = $gradeitem->has_grades();
-                }
-            }
-            $mform->addElement('modgrade', 'scale', get_string('scale'), $gradeoptions);
-            $mform->hideIf('scale', 'assessed', 'eq', 0);
-            $mform->addHelpButton('scale', 'modgrade', 'grades');
-            $mform->setDefault('scale', $CFG->gradepointdefault);
-
-            $mform->addElement('checkbox', 'ratingtime', get_string('ratingtime', 'rating'));
-            $mform->hideIf('ratingtime', 'assessed', 'eq', 0);
-
-            $mform->addElement('date_time_selector', 'assesstimestart', get_string('from'));
-            $mform->hideIf('assesstimestart', 'assessed', 'eq', 0);
-            $mform->disabledIf('assesstimestart', 'ratingtime');
-
-            $mform->addElement('date_time_selector', 'assesstimefinish', get_string('to'));
-            $mform->hideIf('assesstimefinish', 'assessed', 'eq', 0);
-            $mform->disabledIf('assesstimefinish', 'ratingtime');
+            $this->add_rating_settings($mform, 0);
         }
 
-        //doing this here means splitting up the grade related settings on the lesson settings page
-        //$this->standard_grading_coursemodule_elements();
-
         $mform->addElement('header', 'modstandardelshdr', get_string('modstandardels', 'form'));
 
         $section = get_fast_modinfo($COURSE)->get_section_info($this->_section);
@@ -737,6 +709,110 @@ abstract class moodleform_mod extends moodleform {
         $this->plugin_extend_coursemodule_standard_elements();
     }
 
+    /**
+     * Add rating settings.
+     *
+     * @param moodleform_mod $mform
+     * @param int $itemnumber
+     */
+    protected function add_rating_settings($mform, int $itemnumber) {
+        global $CFG, $COURSE;
+
+        if ($this->gradedorrated && $this->gradedorrated !== 'rated') {
+            return;
+        }
+        $this->gradedorrated = 'rated';
+
+        require_once("{$CFG->dirroot}/rating/lib.php");
+        $rm = new rating_manager();
+
+        $component = "mod_{$this->_modname}";
+        $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradecat');
+        $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradepass');
+        $assessedfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'assessed');
+        $scalefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'scale');
+
+        $mform->addElement('header', 'modstandardratings', get_string('ratings', 'rating'));
+
+        $isupdate = !empty($this->_cm);
+
+        $rolenamestring = null;
+        if ($isupdate) {
+            $context = context_module::instance($this->_cm->id);
+            $capabilities = ['moodle/rating:rate', "mod/{$this->_cm->modname}:rate"];
+            $rolenames = get_role_names_with_caps_in_context($context, $capabilities);
+            $rolenamestring = implode(', ', $rolenames);
+        } else {
+            $rolenamestring = get_string('capabilitychecknotavailable', 'rating');
+        }
+
+        $mform->addElement('static', 'rolewarning', get_string('rolewarning', 'rating'), $rolenamestring);
+        $mform->addHelpButton('rolewarning', 'rolewarning', 'rating');
+
+        $mform->addElement('select', $assessedfieldname, get_string('aggregatetype', 'rating') , $rm->get_aggregate_types());
+        $mform->setDefault($assessedfieldname, 0);
+        $mform->addHelpButton($assessedfieldname, 'aggregatetype', 'rating');
+
+        $gradeoptions = [
+            'isupdate' => $isupdate,
+            'currentgrade' => false,
+            'hasgrades' => false,
+            'canrescale' => false,
+            'useratings' => true,
+        ];
+        if ($isupdate) {
+            $gradeitem = grade_item::fetch([
+                'itemtype' => 'mod',
+                'itemmodule' => $this->_cm->modname,
+                'iteminstance' => $this->_cm->instance,
+                'itemnumber' => $itemnumber,
+                'courseid' => $COURSE->id,
+            ]);
+            if ($gradeitem) {
+                $gradeoptions['currentgrade'] = $gradeitem->grademax;
+                $gradeoptions['currentgradetype'] = $gradeitem->gradetype;
+                $gradeoptions['currentscaleid'] = $gradeitem->scaleid;
+                $gradeoptions['hasgrades'] = $gradeitem->has_grades();
+            }
+        }
+
+        $mform->addElement('modgrade', $scalefieldname, get_string('scale'), $gradeoptions);
+        $mform->hideIf($scalefieldname, $assessedfieldname, 'eq', 0);
+        $mform->addHelpButton($scalefieldname, 'modgrade', 'grades');
+        $mform->setDefault($scalefieldname, $CFG->gradepointdefault);
+
+        $mform->addElement('checkbox', 'ratingtime', get_string('ratingtime', 'rating'));
+        $mform->hideIf('ratingtime', $assessedfieldname, 'eq', 0);
+
+        $mform->addElement('date_time_selector', 'assesstimestart', get_string('from'));
+        $mform->hideIf('assesstimestart', $assessedfieldname, 'eq', 0);
+        $mform->hideIf('assesstimestart', 'ratingtime');
+
+        $mform->addElement('date_time_selector', 'assesstimefinish', get_string('to'));
+        $mform->hideIf('assesstimefinish', $assessedfieldname, 'eq', 0);
+        $mform->hideIf('assesstimefinish', 'ratingtime');
+
+        if ($this->_features->gradecat) {
+            $mform->addElement(
+                'select',
+                $gradecatfieldname,
+                get_string('gradecategoryonmodform', 'grades'),
+                grade_get_categories_menu($COURSE->id, $this->_outcomesused)
+            );
+            $mform->addHelpButton($gradecatfieldname, 'gradecategoryonmodform', 'grades');
+            $mform->hideIf($gradecatfieldname, $assessedfieldname, 'eq', 0);
+            $mform->hideIf($gradecatfieldname, "{$scalefieldname}[modgrade_type]", 'eq', 'none');
+        }
+
+        // Grade to pass.
+        $mform->addElement('text', $gradepassfieldname, get_string('gradepass', 'grades'));
+        $mform->addHelpButton($gradepassfieldname, 'gradepass', 'grades');
+        $mform->setDefault($gradepassfieldname, '');
+        $mform->setType($gradepassfieldname, PARAM_RAW);
+        $mform->hideIf($gradepassfieldname, $assessedfieldname, 'eq', '0');
+        $mform->hideIf($gradepassfieldname, "{$scalefieldname}[modgrade_type]", 'eq', 'none');
+    }
+
     /**
      * Plugins can extend the coursemodule settings form.
      */
@@ -811,6 +887,21 @@ abstract class moodleform_mod extends moodleform {
 
     public function standard_grading_coursemodule_elements() {
         global $COURSE, $CFG;
+
+        if ($this->gradedorrated && $this->gradedorrated !== 'graded') {
+            return;
+        }
+        if ($this->_features->rating) {
+            return;
+        }
+        $this->gradedorrated = 'graded';
+
+        $itemnumber = 0;
+        $component = "mod_{$this->_modname}";
+        $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade');
+        $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradecat');
+        $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradepass');
+
         $mform =& $this->_form;
         $isupdate = !empty($this->_cm);
         $gradeoptions = array('isupdate' => $isupdate,
@@ -820,31 +911,27 @@ abstract class moodleform_mod extends moodleform {
                               'useratings' => $this->_features->rating);
 
         if ($this->_features->hasgrades) {
-
-            if (!$this->_features->rating || $this->_features->gradecat) {
+            if ($this->_features->gradecat) {
                 $mform->addElement('header', 'modstandardgrade', get_string('grade'));
             }
 
             //if supports grades and grades arent being handled via ratings
-            if (!$this->_features->rating) {
-
-                if ($isupdate) {
-                    $gradeitem = grade_item::fetch(array('itemtype' => 'mod',
-                                                         'itemmodule' => $this->_cm->modname,
-                                                         'iteminstance' => $this->_cm->instance,
-                                                         'itemnumber' => 0,
-                                                         'courseid' => $COURSE->id));
-                    if ($gradeitem) {
-                        $gradeoptions['currentgrade'] = $gradeitem->grademax;
-                        $gradeoptions['currentgradetype'] = $gradeitem->gradetype;
-                        $gradeoptions['currentscaleid'] = $gradeitem->scaleid;
-                        $gradeoptions['hasgrades'] = $gradeitem->has_grades();
-                    }
+            if ($isupdate) {
+                $gradeitem = grade_item::fetch(array('itemtype' => 'mod',
+                                                        'itemmodule' => $this->_cm->modname,
+                                                        'iteminstance' => $this->_cm->instance,
+                                                        'itemnumber' => 0,
+                                                        'courseid' => $COURSE->id));
+                if ($gradeitem) {
+                    $gradeoptions['currentgrade'] = $gradeitem->grademax;
+                    $gradeoptions['currentgradetype'] = $gradeitem->gradetype;
+                    $gradeoptions['currentscaleid'] = $gradeitem->scaleid;
+                    $gradeoptions['hasgrades'] = $gradeitem->has_grades();
                 }
-                $mform->addElement('modgrade', 'grade', get_string('grade'), $gradeoptions);
-                $mform->addHelpButton('grade', 'modgrade', 'grades');
-                $mform->setDefault('grade', $CFG->gradepointdefault);
             }
+            $mform->addElement('modgrade', $gradefieldname, get_string('grade'), $gradeoptions);
+            $mform->addHelpButton($gradefieldname, 'modgrade', 'grades');
+            $mform->setDefault($gradefieldname, $CFG->gradepointdefault);
 
             if ($this->_features->advancedgrading
                     and !empty($this->current->_advancedgradingdata['methods'])
@@ -858,9 +945,7 @@ abstract class moodleform_mod extends moodleform {
                     $mform->addElement('select', 'advancedgradingmethod_'.$areaname,
                         get_string('gradingmethod', 'core_grading'), $this->current->_advancedgradingdata['methods']);
                     $mform->addHelpButton('advancedgradingmethod_'.$areaname, 'gradingmethod', 'core_grading');
-                    if (!$this->_features->rating) {
-                        $mform->hideIf('advancedgradingmethod_'.$areaname, 'grade[modgrade_type]', 'eq', 'none');
-                    }
+                    $mform->hideIf('advancedgradingmethod_'.$areaname, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
 
                 } else {
                     // the module defines multiple gradable areas, display a selector
@@ -877,25 +962,19 @@ abstract class moodleform_mod extends moodleform {
             }
 
             if ($this->_features->gradecat) {
-                $mform->addElement('select', 'gradecat',
+                $mform->addElement('select', $gradecatfieldname,
                         get_string('gradecategoryonmodform', 'grades'),
                         grade_get_categories_menu($COURSE->id, $this->_outcomesused));
-                $mform->addHelpButton('gradecat', 'gradecategoryonmodform', 'grades');
-                if (!$this->_features->rating) {
-                    $mform->hideIf('gradecat', 'grade[modgrade_type]', 'eq', 'none');
-                }
+                $mform->addHelpButton($gradecatfieldname, 'gradecategoryonmodform', 'grades');
+                $mform->hideIf($gradecatfieldname, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
             }
 
             // Grade to pass.
-            $mform->addElement('text', 'gradepass', get_string('gradepass', 'grades'));
-            $mform->addHelpButton('gradepass', 'gradepass', 'grades');
-            $mform->setDefault('gradepass', '');
-            $mform->setType('gradepass', PARAM_RAW);
-            if (!$this->_features->rating) {
-                $mform->hideIf('gradepass', 'grade[modgrade_type]', 'eq', 'none');
-            } else {
-                $mform->hideIf('gradepass', 'assessed', 'eq', '0');
-            }
+            $mform->addElement('text', $gradepassfieldname, get_string($gradepassfieldname, 'grades'));
+            $mform->addHelpButton($gradepassfieldname, $gradepassfieldname, 'grades');
+            $mform->setDefault($gradepassfieldname, '');
+            $mform->setType($gradepassfieldname, PARAM_RAW);
+            $mform->hideIf($gradepassfieldname, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
         }
     }
 
@@ -1111,5 +1190,3 @@ abstract class moodleform_mod extends moodleform {
         return $data;
     }
 }
-
-
index c5b37fc..3a39c14 100644 (file)
@@ -54,6 +54,9 @@ class core_course_courselib_testcase extends advanced_testcase {
         $moduleinfo->blockperiod = 60*60*24;
         $moduleinfo->blockafter = 10;
         $moduleinfo->warnafter = 5;
+
+        // Grading of whole forum settings.
+        $moduleinfo->grade_forum = 0;
     }
 
     /**
@@ -395,6 +398,9 @@ class core_course_courselib_testcase extends advanced_testcase {
         $moduleinfo->blockperiod = 60*60*24;
         $moduleinfo->blockafter = 10;
         $moduleinfo->warnafter = 5;
+
+        // Grading of whole forum settings.
+        $moduleinfo->grade_forum = 0;
     }
 
     /**
index ab857e9..1da624c 100644 (file)
@@ -2065,7 +2065,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $outcomegradeitem->cmid = 0;
         $outcomegradeitem->courseid = $course->id;
         $outcomegradeitem->aggregationcoef = 0;
-        $outcomegradeitem->itemnumber = 1; // The activity's original grade item will be 0.
+        $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
         $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
         $outcomegradeitem->scaleid = $outcome->scaleid;
         $outcomegradeitem->insert();
@@ -2384,8 +2384,8 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(2, $result['courses']);
 
         // Check default filters.
-        $this->assertCount(3, $result['courses'][0]['filters']);
-        $this->assertCount(3, $result['courses'][1]['filters']);
+        $this->assertCount(4, $result['courses'][0]['filters']);
+        $this->assertCount(4, $result['courses'][1]['filters']);
 
         $result = core_course_external::get_courses_by_field('category', $category1->id);
         $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
@@ -2427,7 +2427,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
 
         // Check default filters.
         $filters = $result['courses'][0]['filters'];
-        $this->assertCount(3, $filters);
+        $this->assertCount(4, $filters);
         $found = false;
         foreach ($filters as $filter) {
             if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
@@ -2988,4 +2988,55 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
         $this->assertCount(1, $result);
         $this->assertEquals($courses[0]->id, array_shift($result)->id);
     }
+
+    /**
+     * Test get enrolled users by cmid function.
+     */
+    public function test_get_enrolled_users_by_cmid() {
+        $this->resetAfterTest(true);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // Set the first created user to the test user.
+        self::setUser($user1);
+
+        // Create course to add the module.
+        $course1 = self::getDataGenerator()->create_course();
+
+        // Forum with tracking off.
+        $record = new stdClass();
+        $record->course = $course1->id;
+        $forum1 = self::getDataGenerator()->create_module('forum', $record);
+
+        // Following lines enrol and assign default role id to the users.
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+
+        // Create what we expect to be returned when querying the course module.
+        $expectedusers = array(
+            'users' => array(),
+            'warnings' => array(),
+        );
+
+        $expectedusers['users'][0] = [
+            'id' => $user1->id,
+            'fullname' => fullname($user1),
+            'firstname' => $user1->firstname,
+            'lastname' => $user1->lastname,
+        ];
+        $expectedusers['users'][1] = [
+            'id' => $user2->id,
+            'fullname' => fullname($user2),
+            'firstname' => $user2->firstname,
+            'lastname' => $user2->lastname,
+        ];
+
+        // Test getting the users in a given context.
+        $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
+        $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
+
+        $this->assertEquals(2, count($users['users']));
+        $this->assertEquals($expectedusers, $users);
+    }
 }
diff --git a/filter/displayh5p/db/install.php b/filter/displayh5p/db/install.php
new file mode 100644 (file)
index 0000000..a5a03e0
--- /dev/null
@@ -0,0 +1,38 @@
+<?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/>.
+
+/**
+ * Display H5P active by default
+ *
+ * @package    filter_displayh5p
+ * @copyright  2019 Amaia Anabitarte <amaia@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Enable displayh5p filter by default to render H5P contents.
+ * @throws coding_exception
+ */
+function xmldb_filter_displayh5p_install() {
+    global $CFG;
+
+    require_once("$CFG->libdir/filterlib.php");
+
+    // Display H5P filter should be enabled by default because we need this filter for H5P atto button to work.
+    filter_set_global_state('displayh5p', TEXTFILTER_ON, -1);
+}
index bd31b68..d13ff33 100644 (file)
@@ -47,49 +47,89 @@ class filter_displayh5p extends moodle_text_filter {
      * @return string
      */
     public function filter($text, array $options = array()) {
+        global $CFG;
 
         if (!is_string($text) or empty($text)) {
             // Non string data can not be filtered anyway.
             return $text;
         }
 
-        if (stripos($text, 'http') === false) {
+        // We are trying to minimize performance impact checking there's some H5P related URL.
+        $h5purl = '(http[^ &<]*h5p)';
+        if (!preg_match($h5purl, $text)) {
             return $text;
         }
 
         $allowedsources = get_config('filter_displayh5p', 'allowedsources');
         $allowedsources = array_filter(array_map('trim', explode("\n", $allowedsources)));
-        if (empty($allowedsources)) {
-            return $text;
-        }
+
+        $localsource = '('.preg_quote($CFG->wwwroot).'/[^ &<]*\.h5p([?][^ <]*)?[^ &<]*)';
+        $allowedsources[] = $localsource;
 
         $params = array(
-            'tagbegin' => "<iframe src=",
-            'tagend' => "</iframe>"
+            'tagbegin' => '<iframe src="',
+            'tagend' => '</iframe>'
         );
 
+        $specialchars = ['*', '?', '&', '[^<]'];
+        $escapedspecialchars = ['[^.]+', '\?', '&amp;', '[^<]*'];
+        $h5pcontents = array();
+
+        // Check all allowed sources.
         foreach ($allowedsources as $source) {
             // It is needed to add "/embed" at the end of URLs like https:://*.h5p.com/content/12345 (H5P.com).
             $params['urlmodifier'] = '';
-            if (!(stripos($source, 'embed'))) {
-                $params['urlmodifier'] = '/embed';
+
+            if (($source == $localsource)) {
+                $params['tagbegin'] = '<iframe src="'.$CFG->wwwroot.'/h5p/embed.php?url=';
+                $ultimatepattern = '#'.$source.'#';
+            } else {
+                if (!stripos($source, 'embed')) {
+                    $params['urlmodifier'] = '/embed';
+                }
+                // Convert special chars.
+                $sourceid = str_replace('[id]', '[0-9]+', $source);
+                $escapechars = str_replace($specialchars, $escapedspecialchars, $sourceid);
+                $ultimatepattern = '#(' . $escapechars . ')#';
             }
 
-            // Convert special chars.
-            $specialchars = ['*', '?', '&'];
-            $escapedspecialchars = ['[^.]+', '\?', '&amp;'];
-            $sourceid = str_replace('[id]', '[0-9]+', $source);
-            $escapechars = str_replace($specialchars, $escapedspecialchars, $sourceid);
-            $ultimatepattern = '#(' . $escapechars . ')#';
+            // Improve performance creating filterobjects only when needed.
+            if (!preg_match($ultimatepattern, $text)) {
+                continue;
+            }
 
             $h5pcontenturl = new filterobject($source, null, null, false,
-                   false, null, [$this, 'filterobject_prepare_replacement_callback'], $params);
+                false, null, [$this, 'filterobject_prepare_replacement_callback'], $params);
 
             $h5pcontenturl->workregexp = $ultimatepattern;
             $h5pcontents[] = $h5pcontenturl;
         }
 
-        return filter_phrases($text, $h5pcontents, null, null, false, true);
+        if (empty($h5pcontents)) {
+            // No matches to deal with.
+            return $text;
+        }
+
+        $result = filter_phrases($text, $h5pcontents, null, null, false, true);
+
+        // Encoding H5P file URLs.
+        // embed.php page is requesting a PARAM_LOCALURL url parameter, so for files/directories use non-alphanumeric
+        // characters, we need to encode the parameter. Fetch url parameter added to embed.php and encode the whole url.
+        $localurl = '#\?url=([^" <]*[\/]+[^" <]*\.h5p)([?][^"]*)?#';
+        $result = preg_replace_callback($localurl,
+            function ($matches) {
+                $baseurl = rawurlencode($matches[1]);
+                // Deal with possible parameters in the url link.
+                if (!empty($matches[2])) {
+                    $match = explode('?', $matches[2]);
+                    if (!empty($match[1])) {
+                        $baseurl = $baseurl."&".$match[1];
+                    }
+                }
+                return "?url=".$baseurl;
+            }, $result);
+
+        return $result;
     }
 
     /**
@@ -101,13 +141,13 @@ class filter_displayh5p extends moodle_text_filter {
      * @return array [$hreftagbegin, $hreftagend, $replacementphrase] for filterobject.
      */
     public function filterobject_prepare_replacement_callback($tagbegin, $tagend, $urlmodifier) {
-
         $sourceurl = "$1";
         if ($urlmodifier !== "") {
             $sourceurl .= $urlmodifier;
         }
 
-        $h5piframesrc = "\"".$sourceurl."\" width=\"100%\" height=\"637\" allowfullscreen=\"allowfullscreen\" style=\"border: 0;\">";
+        $h5piframesrc = $sourceurl.
+            '" class="h5p-iframe" style="height:230px; width: 100%; border: 0;" allowfullscreen="allowfullscreen">';
 
         // We want to request the resizing script only once.
         if (self::$loadresizerjs) {
index 2ac3aae..a8313c8 100644 (file)
@@ -45,8 +45,6 @@ class filter_displayh5p_testcase extends advanced_testcase {
             "https://h5p.org/h5p/embed/[id]\nhttps://*.h5p.com/content/[id]/embed\nhttps://*.h5p.com/content/[id]
                 \nhttps://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php?action=h5p_embed&id=[id]",
             'filter_displayh5p');
-        // Enable display h5p filter at top level.
-        filter_set_global_state('displayh5p', TEXTFILTER_ON);
     }
 
     /**
@@ -71,6 +69,8 @@ class filter_displayh5p_testcase extends advanced_testcase {
      * @return array
      */
     public function texts_provider() {
+        global $CFG;
+
         return [
             ["http:://example.com", "#http:://example.com#"],
             ["http://google.es/h5p/embed/3425234", "#http://google.es/h5p/embed/3425234#"],
@@ -83,7 +83,13 @@ class filter_displayh5p_testcase extends advanced_testcase {
             ["https://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php?action=h5p_embed&amp;id=13",
                     "#<iframe src=\"https://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php\?action=h5p_embed\&amp\;id=13\"[^>]+?>#"],
             ["https://h5p.org/h5p/embed/547225 another content in the same page https://moodle.h5p.com/content/1290729733828858779/embed",
-                    "#<iframe src=\"https://h5p.org/h5p/embed/547225\"[^>]+?>((?!<iframe).)*<iframe src=\"https://moodle.h5p.com/content/1290729733828858779/embed\"[^>]+?>#"]
+                    "#<iframe src=\"https://h5p.org/h5p/embed/547225\"[^>]+?>((?!<iframe).)*<iframe src=\"https://moodle.h5p.com/content/1290729733828858779/embed\"[^>]+?>#"],
+            [$CFG->wwwroot."/pluginfile.php/5/user/private/interactive-video.h5p?export=1&embed=1",
+                    "#<iframe src=\"{$CFG->wwwroot}/h5p/embed.php\?url=".rawurlencode("{$CFG->wwwroot}/pluginfile.php/5/user/private/interactive-video.h5p").
+                    "&export=1&embed=1\"[^>]*?></iframe>#"],
+            [$CFG->wwwroot."/pluginfile.php/5/user/private/accordion-6-7138%20%281%29.h5p.h5p",
+                    "#<iframe src=\"{$CFG->wwwroot}/h5p/embed.php\?url=".rawurlencode("{$CFG->wwwroot}/pluginfile.php/5/user/private/accordion-6-7138%20%281%29.h5p.h5p").
+                    "\"[^>]*?></iframe>#"]
         ];
     }
 }
\ No newline at end of file
diff --git a/grade/amd/build/grades/grader/gradingpanel/normalise.min.js b/grade/amd/build/grades/grader/gradingpanel/normalise.min.js
new file mode 100644 (file)
index 0000000..7ee975f
Binary files /dev/null and b/grade/amd/build/grades/grader/gradingpanel/normalise.min.js differ
diff --git a/grade/amd/build/grades/grader/gradingpanel/normalise.min.js.map b/grade/amd/build/grades/grader/gradingpanel/normalise.min.js.map
new file mode 100644 (file)
index 0000000..c1ae5e7
Binary files /dev/null and b/grade/amd/build/grades/grader/gradingpanel/normalise.min.js.map differ
diff --git a/grade/amd/build/grades/grader/gradingpanel/point.min.js b/grade/amd/build/grades/grader/gradingpanel/point.min.js
new file mode 100644 (file)
index 0000000..e7096de
Binary files /dev/null and b/grade/amd/build/grades/grader/gradingpanel/point.min.js differ
diff --git a/grade/amd/build/grades/grader/gradingpanel/point.min.js.map b/grade/amd/build/grades/grader/gradingpanel/point.min.js.map
new file mode 100644 (file)
index 0000000..7bc1669
Binary files /dev/null and b/grade/amd/build/grades/grader/gradingpanel/point.min.js.map differ
diff --git a/grade/amd/build/grades/grader/gradingpanel/repository.min.js b/grade/amd/build/grades/grader/gradingpanel/repository.min.js
new file mode 100644 (file)
index 0000000..34faeff
Binary files /dev/null and b/grade/amd/build/grades/grader/gradingpanel/repository.min.js differ
diff --git a/grade/amd/build/grades/grader/gradingpanel/repository.min.js.map b/grade/amd/build/grades/grader/gradingpanel/repository.min.js.map
new file mode 100644 (file)
index 0000000..2fd7938
Binary files /dev/null and b/grade/amd/build/grades/grader/gradingpanel/repository.min.js.map differ
diff --git a/grade/amd/build/grades/grader/gradingpanel/scale.min.js b/grade/amd/build/grades/grader/gradingpanel/scale.min.js
new file mode 100644 (file)
index 0000000..edb9c07
Binary files /dev/null and b/grade/amd/build/grades/grader/gradingpanel/scale.min.js differ
diff --git a/grade/amd/build/grades/grader/gradingpanel/scale.min.js.map b/grade/amd/build/grades/grader/gradingpanel/scale.min.js.map
new file mode 100644 (file)
index 0000000..7ceeb00
Binary files /dev/null and b/grade/amd/build/grades/grader/gradingpanel/scale.min.js.map differ
diff --git a/grade/amd/src/grades/grader/gradingpanel/normalise.js b/grade/amd/src/grades/grader/gradingpanel/normalise.js
new file mode 100644 (file)
index 0000000..2ac0bea
--- /dev/null
@@ -0,0 +1,67 @@
+// 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/>.
+
+/**
+ * Repository for simple direct grading panel.
+ *
+ * @module     core_grades/grades/grader/gradingpanel/repository
+ * @package    core_grades
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Normalise a resultset for consumption by the grader.
+ *
+ * @param {Object} result The result returned from a grading web service
+ * @return {Object}
+ */
+export const normaliseResult = result => {
+    return {
+        result,
+        failed: !!result.warnings.length,
+        success: !result.warnings.length,
+        error: null,
+    };
+};
+
+/**
+ * Return the resultset used to describe an invalid result.
+ *
+ * @return {Object}
+ */
+export const invalidResult = () => {
+    return {
+        success: false,
+        failed: false,
+        result: {},
+        error: null,
+    };
+};
+
+/**
+ * Return the resultset used to describe a failed update.
+ *
+ * @param {Object} error
+ * @return {Object}
+ */
+export const failedUpdate = error => {
+    return {
+        success: false,
+        failed: true,
+        result: {},
+        error,
+    };
+};
diff --git a/grade/amd/src/grades/grader/gradingpanel/point.js b/grade/amd/src/grades/grader/gradingpanel/point.js
new file mode 100644 (file)
index 0000000..5dea4db
--- /dev/null
@@ -0,0 +1,61 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Grading panel for simple direct grading.
+ *
+ * @module     core_grades/grades/grader/gradingpanel/point
+ * @package    core_grades
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {saveGrade, fetchGrade} from './repository';
+// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
+import jQuery from 'jquery';
+import {invalidResult} from './normalise';
+
+/**
+ * Fetch the current grade for a user.
+ *
+ * @param {String} component
+ * @param {Number} context
+ * @param {String} itemname
+ * @param {Number} userId
+ * @param {Element} rootNode
+ * @return {Object}
+ */
+export const fetchCurrentGrade = (...args) => fetchGrade('point')(...args);
+
+/**
+ * Store a new grade for a user.
+ *
+ * @param {String} component
+ * @param {Number} context
+ * @param {String} itemname
+ * @param {Number} userId
+ * @param {Element} rootNode
+ * @return {Object}
+ */
+export const storeCurrentGrade = async(component, context, itemname, userId, rootNode) => {
+    const form = rootNode.querySelector('form');
+    const grade = form.querySelector('input[name="grade"]');
+
+    if (!grade.checkValidity() || !grade.value.trim()) {
+        return invalidResult;
+    }
+
+    return await saveGrade('point')(component, context, itemname, userId, jQuery(form).serialize());
+};
diff --git a/grade/amd/src/grades/grader/gradingpanel/repository.js b/grade/amd/src/grades/grader/gradingpanel/repository.js
new file mode 100644 (file)
index 0000000..c7a51bd
--- /dev/null
@@ -0,0 +1,50 @@
+// 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/>.
+
+/**
+ * Repository for simple direct grading panel.
+ *
+ * @module     core_grades/grades/grader/gradingpanel/repository
+ * @package    core_grades
+ * @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';
+import {normaliseResult} from './normalise';
+
+export const fetchGrade = type => (component, contextid, itemname, gradeduserid) => {
+    return fetchMany([{
+        methodname: `core_grades_grader_gradingpanel_${type}_fetch`,
+        args: {
+            component,
+            contextid,
+            itemname,
+            gradeduserid,
+        },
+    }])[0];
+};
+
+export const saveGrade = type => async(component, contextid, itemname, gradeduserid, formdata) => {
+    return normaliseResult(await fetchMany([{
+        methodname: `core_grades_grader_gradingpanel_${type}_store`,
+        args: {
+            component,
+            contextid,
+            itemname,
+            gradeduserid,
+            formdata,
+        },
+    }])[0]);
+};
diff --git a/grade/amd/src/grades/grader/gradingpanel/scale.js b/grade/amd/src/grades/grader/gradingpanel/scale.js
new file mode 100644 (file)
index 0000000..3caca5e
--- /dev/null
@@ -0,0 +1,41 @@
+// 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 simple direct grading.
+ *
+ * @module     core_grades/grades/grader/gradingpanel/scale
+ * @package    core_grades
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {saveGrade, fetchGrade} from './repository';
+// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
+import jQuery from 'jquery';
+import {invalidResult} from './normalise';
+
+export const fetchCurrentGrade = (...args) => fetchGrade('scale')(...args);
+
+export const storeCurrentGrade = (component, context, itemname, userId, rootNode) => {
+    const form = rootNode.querySelector('form');
+    const grade = form.querySelector('select[name="grade"]');
+
+    if (!grade.checkValidity() || !grade.value.trim()) {
+        return invalidResult;
+    }
+
+    return saveGrade('scale')(component, context, itemname, userId, jQuery(form).serialize());
+};
diff --git a/grade/classes/component_gradeitem.php b/grade/classes/component_gradeitem.php
new file mode 100644 (file)
index 0000000..a8f6b34
--- /dev/null
@@ -0,0 +1,477 @@
+<?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/>.
+
+/**
+ * Compontent definition of a gradeitem.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace core_grades;
+
+use context;
+use gradingform_controller;
+use gradingform_instance;
+use moodle_exception;
+use stdClass;
+use grade_item as core_gradeitem;
+use grading_manager;
+
+/**
+ * Compontent definition of a gradeitem.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class component_gradeitem {
+
+    /** @var array The scale data for the current grade item */
+    protected $scale;
+
+    /** @var string The component */
+    protected $component;
+
+    /** @var context The context for this activity */
+    protected $context;
+
+    /** @var string The item name */
+    protected $itemname;
+
+    /** @var int The grade itemnumber */
+    protected $itemnumber;
+
+    /**
+     * component_gradeitem constructor.
+     *
+     * @param string $component
+     * @param context $context
+     * @param string $itemname
+     * @throws \coding_exception
+     */
+    final protected function __construct(string $component, context $context, string $itemname) {
+        $this->component = $component;
+        $this->context = $context;
+        $this->itemname = $itemname;
+        $this->itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
+    }
+
+    /**
+     * Fetch an instance of a specific component_gradeitem.
+     *
+     * @param string $component
+     * @param context $context
+     * @param string $itemname
+     * @return self
+     */
+    public static function instance(string $component, context $context, string $itemname): self {
+        $itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
+
+        $classname = "{$component}\\grades\\{$itemname}_gradeitem";
+        if (!class_exists($classname)) {
+            throw new coding_exception("Unknown gradeitem {$itemname} for component {$classname}");
+        }
+
+        return $classname::load_from_context($context);
+    }
+
+    /**
+     * Load an instance of the current component_gradeitem based on context.
+     *
+     * @param context $context
+     * @return self
+     */
+    abstract public static function load_from_context(context $context): self;
+
+    /**
+     * The table name used for grading.
+     *
+     * @return string
+     */
+    abstract protected function get_table_name(): string;
+
+    /**
+     * Get the itemid for the current gradeitem.
+     *
+     * @return int
+     */
+    public function get_grade_itemid(): int {
+        return component_gradeitems::get_itemnumber_from_itemname($this->component, $this->itemname);
+    }
+
+    /**
+     * Whether grading is enabled for this item.
+     *
+     * @return bool
+     */
+    abstract public function is_grading_enabled(): bool;
+
+    /**
+     * Get the grade value for this instance.
+     * The itemname is translated to the relevant grade field for the activity.
+     *
+     * @return int
+     */
+    abstract protected function get_gradeitem_value(): ?int;
+
+    /**
+     * Whether the grader can grade the gradee.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return bool
+     */
+    abstract public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool;
+
+    /**
+     * Require that the user can grade, throwing an exception if not.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @throws required_capability_exception
+     */
+    abstract public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void;
+
+    /**
+     * Get the scale if a scale is being used.
+     *
+     * @return stdClass
+     */
+    protected function get_scale(): ?stdClass {
+        global $DB;
+
+        $gradetype = $this->get_gradeitem_value();
+        if ($gradetype > 0) {
+            return null;
+        }
+
+        // This is a scale.
+        if (null === $this->scale) {
+            $this->scale = $DB->get_record('scale', ['id' => -1 * $gradetype]);
+        }
+
+        return $this->scale;
+    }
+
+    /**
+     * Check whether a scale is being used for this grade item.
+     *
+     * @return bool
+     */
+    public function is_using_scale(): bool {
+        $gradetype = $this->get_gradeitem_value();
+
+        return $gradetype < 0;
+    }
+
+    /**
+     * Whether this grade item is configured to use direct grading.
+     *
+     * @return bool
+     */
+    public function is_using_direct_grading(): bool {
+        if ($this->is_using_scale()) {
+            return false;
+        }
+
+        if ($this->get_advanced_grading_controller()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Whether this grade item is configured to use advanced grading.
+     *
+     * @return bool
+     */
+    public function is_using_advanced_grading(): bool {
+        if ($this->is_using_scale()) {
+            return false;
+        }
+
+        if ($this->get_advanced_grading_controller()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the name of the advanced grading method.
+     *
+     * @return string
+     */
+    public function get_advanced_grading_method(): ?string {
+        $gradingmanager = $this->get_grading_manager();
+
+        if (empty($gradingmanager)) {
+            return null;
+        }
+
+        return $gradingmanager->get_active_method();
+    }
+
+    /**
+     * Get the name of the component responsible for grading this gradeitem.
+     *
+     * @return string
+     */
+    public function get_grading_component_name(): ?string {
+        if (!$this->is_grading_enabled()) {
+            return null;
+        }
+
+        if ($method = $this->get_advanced_grading_method()) {
+            return "gradingform_{$method}";
+        }
+
+        return 'core_grades';
+    }
+
+    /**
+     * Get the name of the component subtype responsible for grading this gradeitem.
+     *
+     * @return string
+     */
+    public function get_grading_component_subtype(): ?string {
+        if (!$this->is_grading_enabled()) {
+            return null;
+        }
+
+        if ($method = $this->get_advanced_grading_method()) {
+            return null;
+        }
+
+        if ($this->is_using_scale()) {
+            return 'scale';
+        }
+
+        return 'point';
+    }
+
+    /**
+     * Whether decimals are allowed.
+     *
+     * @return bool
+     */
+    protected function allow_decimals(): bool {
+        return $this->get_gradeitem_value() > 0;
+    }
+
+    /**
+     * Get the grading manager for this advanced grading definition.
+     *
+     * @return grading_manager
+     */
+    protected function get_grading_manager(): ?grading_manager {
+        require_once(__DIR__ . '/../grading/lib.php');
+        return get_grading_manager($this->context, $this->component, $this->itemname);
+
+    }
+
+    /**
+     * Get the advanced grading controller if advanced grading is enabled.
+     *
+     * @return gradingform_controller
+     */
+    protected function get_advanced_grading_controller(): ?gradingform_controller {
+        $gradingmanager = $this->get_grading_manager();
+
+        if (empty($gradingmanager)) {
+            return null;
+        }
+
+        if ($gradingmethod = $gradingmanager->get_active_method()) {
+            return $gradingmanager->get_controller($gradingmethod);
+        }
+
+        return null;
+    }
+
+    /**
+     * Get the list of available grade items.
+     *
+     * @return array
+     */
+    public function get_grade_menu(): array {
+        return make_grades_menu($this->get_gradeitem_value());
+    }
+
+    /**
+     * Check whether the supplied grade is valid and throw an exception if not.
+     *
+     * @param float $grade The value being checked
+     * @throws moodle_exception
+     * @return bool
+     */
+    public function check_grade_validity(?float $grade): bool {
+        $grade = grade_floatval(unformat_float($grade));
+        if ($grade) {
+            if ($this->is_using_scale()) {
+                // Fetch all options for this scale.
+                $scaleoptions = make_menu_from_list($this->get_scale()->scale);
+
+                if ($grade != -1 && !array_key_exists((int) $grade, $scaleoptions)) {
+                    // The selected option did not exist.
+                    throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
+                        'maxgrade' => count($scaleoptions),
+                        'grade' => $grade,
+                    ]);
+                }
+            } else if ($grade) {
+                $maxgrade = $this->get_gradeitem_value();
+                if ($grade > $maxgrade) {
+                    // The grade is greater than the maximum possible value.
+                    throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
+                        'maxgrade' => $maxgrade,
+                        'grade' => $grade,
+                    ]);
+                } else if ($grade < 0) {
+                    // Negative grades are not supported.
+                    throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
+                        'maxgrade' => $maxgrade,
+                        'grade' => $grade,
+                    ]);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Create an empty row in the grade for the specified user and grader.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return stdClass The newly created grade record
+     */
+    abstract public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass;
+
+    /**
+     * Get the grade record for the specified grade id.
+     *
+     * @param int $gradeid
+     * @return stdClass
+     * @throws \dml_exception
+     */
+    public function get_grade(int $gradeid): stdClass {
+        global $DB;
+
+        $grade = $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
+
+        return $grade ?: null;
+    }
+
+    /**
+     * Get the grade for the specified user.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @return stdClass The grade value
+     */
+    abstract public function get_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass;
+
+    /**
+     * Get grades for all users for the specified gradeitem.
+     *
+     * @return stdClass[] The grades
+     */
+    abstract public function get_all_grades(): array;
+
+    /**
+     * Create or update the grade.
+     *
+     * @param stdClass $grade
+     * @return bool Success
+     */
+    abstract protected function store_grade(stdClass $grade): bool;
+
+    /**
+     * Create or update the grade.
+     *
+     * @param stdClass $gradeduser The user being graded
+     * @param stdClass $grader The user who is grading
+     * @param stdClass $formdata The data submitted
+     * @return bool Success
+     */
+    public function store_grade_from_formdata(stdClass $gradeduser, stdClass $grader, stdClass $formdata): bool {
+        // Require gradelib for grade_floatval.
+        require_once(__DIR__ . '/../../lib/gradelib.php');
+        $grade = $this->get_grade_for_user($gradeduser, $grader);
+
+        if ($this->is_using_advanced_grading()) {
+            $instanceid = $formdata->instanceid;
+            $gradinginstance = $this->get_advanced_grading_instance($grader, $grade, (int) $instanceid);
+            $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id);
+
+            if ($grade->grade == -1) {
+                // In advanced grading, a value of -1 means no data.
+                return false;
+            }
+        } else {
+            // Handle the case when grade is set to No Grade.
+            if (isset($formdata->grade)) {
+                $grade->grade = grade_floatval(unformat_float($formdata->grade));
+            }
+        }
+
+        return $this->store_grade($grade);
+    }
+
+    /**
+     * Get the advanced grading instance for the specified grade entry.
+     *
+     * @param stdClass $grader The user who is grading
+     * @param stdClass $grade The row from the grade table.
+     * @param int $instanceid The instanceid of the advanced grading form
+     * @return gradingform_instance
+     */
+    public function get_advanced_grading_instance(stdClass $grader, stdClass $grade, int $instanceid = null): ?gradingform_instance {
+        $controller = $this->get_advanced_grading_controller($this->itemname);
+
+        if (empty($controller)) {
+            // Advanced grading not enabeld for this item.
+            return null;
+        }
+
+        if (!$controller->is_form_available()) {
+            // The form is not available for this item.
+            return null;
+        }
+
+        // Fetch the instance for the specified graderid/itemid.
+        $gradinginstance = $controller->fetch_instance(
+            (int) $grader->id,
+            (int) $grade->id,
+            $instanceid
+        );
+
+        // Set the allowed grade range.
+        $gradinginstance->get_controller()->set_grade_range(
+            $this->get_grade_menu(),
+            $this->allow_decimals()
+        );
+
+        return $gradinginstance;
+    }
+}
diff --git a/grade/classes/component_gradeitems.php b/grade/classes/component_gradeitems.php
new file mode 100644 (file)
index 0000000..daa260d
--- /dev/null
@@ -0,0 +1,219 @@
+<?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/>.
+
+/**
+ * Helper class to fetch information about component grade items.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace core_grades;
+
+use code_grades\local\gradeitem\itemnumber_mapping;
+use code_grades\local\gradeitem\advancedgrading_mapping;
+
+/**
+ * Helper class to fetch information about component grade items.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class component_gradeitems {
+
+    /**
+     * Get the gradeitems classname for the specific component.
+     *
+     * @param string $component The component to fetch the classname for
+     * @return string The composed classname
+     */
+    protected static function get_component_classname(string $component): string {
+        return "{$component}\\grades\gradeitems";
+    }
+
+    /**
+     * Get the grade itemnumber mapping for a component.
+     *
+     * @param string $component The component that the grade item belongs to
+     * @return array
+     */
+    public static function get_itemname_mapping_for_component(string $component): array {
+        $classname = "{$component}\\grades\gradeitems";
+
+        if (!class_exists($classname)) {
+            return [
+                0 => '',
+            ];
+        }
+
+        if (!is_subclass_of($classname, 'core_grades\local\gradeitem\itemnumber_mapping')) {
+            throw new \coding_exception("The {$classname} class does not implement " . itemnumber_mapping::class);
+        }
+
+        return $classname::get_itemname_mapping_for_component();
+    }
+
+    /**
+     * Whether the named grading item exists.
+     *
+     * @param string $component
+     * @param string $itemname
+     * @return bool
+     */
+    public static function is_valid_itemname(string $component, string $itemname): bool {
+        $items = self::get_itemname_mapping_for_component($component);
+
+        return array_search($itemname, $items) !== false;
+    }
+
+    /**
+     * Check whether the component class defines the advanced grading items.
+     *
+     * @param string $component The component to check
+     * @return bool
+     */
+    public static function defines_advancedgrading_itemnames_for_component(string $component): bool {
+        return is_subclass_of(self::get_component_classname($component), 'core_grades\local\gradeitem\advancedgrading_mapping');
+    }
+
+    /**
+     * Get the list of advanced grading item names for the named component.
+     *
+     * @param string $component
+     * @return array
+     */
+    public static function get_advancedgrading_itemnames_for_component(string $component): array {
+        $classname = self::get_component_classname($component);
+        if (!self::defines_advancedgrading_itemnames_for_component($component)) {
+            throw new \coding_exception("The {$classname} class does not implement " . advancedgrading_mapping::class);
+        }
+
+        return $classname::get_advancedgrading_itemnames();
+    }
+
+    /**
+     * Whether the named grading item name supports advanced grading.
+     *
+     * @param string $component
+     * @param string $itemname
+     * @return bool
+     */
+    public static function is_advancedgrading_itemname(string $component, string $itemname): bool {
+        $gradingareas = self::get_advancedgrading_itemnames_for_component($component);
+
+        return array_search($itemname, $gradingareas) !== false;
+    }
+
+    /**
+     * Get the suffixed field name for an activity field mapped from its itemnumber.
+     *
+     * For legacy reasons, the first itemnumber has no suffix on field names.
+     *
+     * @param string $component The component that the grade item belongs to
+     * @param int $itemnumber The grade itemnumber
+     * @param string $fieldname The name of the field to be rewritten
+     * @return string The translated field name
+     */
+    public static function get_field_name_for_itemnumber(string $component, int $itemnumber, string $fieldname): string {
+        $itemname = static::get_itemname_from_itemnumber($component, $itemnumber);
+
+        if ($itemname) {
+            return "{$fieldname}_{$itemname}";
+        }
+
+        return $fieldname;
+    }
+
+    /**
+     * Get the suffixed field name for an activity field mapped from its itemnumber.
+     *
+     * For legacy reasons, the first itemnumber has no suffix on field names.
+     *
+     * @param string $component The component that the grade item belongs to
+     * @param string $itemname The grade itemname
+     * @param string $fieldname The name of the field to be rewritten
+     * @return string The translated field name
+     */
+    public static function get_field_name_for_itemname(string $component, string $itemname, string $fieldname): string {
+        if (empty($itemname)) {
+            return $fieldname;
+        }
+
+        $itemnumber = static::get_itemnumber_from_itemname($component, $itemname);
+
+        if ($itemnumber > 0) {
+            return "{$fieldname}_{$itemname}";
+        }
+
+        return $fieldname;
+    }
+
+    /**
+     * Get the itemname for an itemnumber.
+     *
+     * For legacy compatability when the itemnumber is 0, the itemname will always be empty.
+     *
+     * @param string $component The component that the grade item belongs to
+     * @param int $itemnumber The grade itemnumber
+     * @return int The grade itemnumber of the itemname
+     */
+    public static function get_itemname_from_itemnumber(string $component, int $itemnumber): string {
+        if ($itemnumber === 0) {
+            return '';
+        }
+
+        $mappings = self::get_itemname_mapping_for_component($component);
+
+        if (isset($mappings[$itemnumber])) {
+            return $mappings[$itemnumber];
+        }
+
+        if ($itemnumber >= 1000) {
+            // An itemnumber >= 1000 belongs to an outcome.
+            return '';
+        }
+
+        throw new \coding_exception("Unknown itemnumber mapping for {$itemnumber} in {$component}");
+    }
+
+    /**
+     * Get the itemnumber for a item name.
+     *
+     * For legacy compatability when the itemname is empty, the itemnumber will always be 0.
+     *
+     * @param string $component The component that the grade item belongs to
+     * @param string $itemname The grade itemname
+     * @return int The grade itemname of the itemnumber
+     */
+    public static function get_itemnumber_from_itemname(string $component, string $itemname): int {
+        if (empty($itemname)) {
+            return 0;
+        }
+
+        $mappings = self::get_itemname_mapping_for_component($component);
+
+        $flipped = array_flip($mappings);
+        if (isset($flipped[$itemname])) {
+            return $flipped[$itemname];
+        }
+
+        throw new \coding_exception("Unknown itemnumber mapping for {$itemname} in {$component}");
+    }
+}
diff --git a/grade/classes/grades/grader/gradingpanel/point/external/fetch.php b/grade/classes/grades/grader/gradingpanel/point/external/fetch.php
new file mode 100644 (file)
index 0000000..b8332e5
--- /dev/null
@@ -0,0 +1,172 @@
+<?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 service functions relating to point grades and grading.
+ *
+ * @package    core_grades
+ * @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 core_grades\grades\grader\gradingpanel\point\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_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use required_capability_exception;
+use stdClass;
+
+/**
+ * External grading panel point API
+ *
+ * @package    core_grades
+ * @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
+     * @throws \dml_exception
+     * @throws \invalid_parameter_exception
+     * @throws \restricted_context_exception
+     * @throws coding_exception
+     * @throws moodle_exception
+     * @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 (!$gradeitem->is_using_direct_grading()) {
+            throw new moodle_exception("The {$itemname} item in {$component}/{$contextid} is not configured for direct grading");
+        }
+
+        // Fetch the actual data.
+        $gradeduser = \core_user::get_user($gradeduserid);
+        $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+
+        return self::get_fetch_data($grade);
+    }
+
+    /**
+     * Get the data to be fetched.
+     *
+     * @param stdClass $grade
+     * @return array
+     */
+    public static function get_fetch_data(stdClass $grade): array {
+        return [
+            'templatename' => 'core_grades/grades/grader/gradingpanel/point',
+            'grade' => [
+                'grade' => $grade->grade,
+                '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([
+                'grade' => new external_value(PARAM_FLOAT, 'The numeric grade'),
+                '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(),
+        ]);
+    }
+}
diff --git a/grade/classes/grades/grader/gradingpanel/point/external/store.php b/grade/classes/grades/grader/gradingpanel/point/external/store.php
new file mode 100644 (file)
index 0000000..3649e53
--- /dev/null
@@ -0,0 +1,170 @@
+<?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 service functions relating to point grades and grading.
+ *
+ * @package    core_grades
+ * @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 core_grades\grades\grader\gradingpanel\point\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_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use required_capability_exception;
+
+/**
+ * External grading panel point API
+ *
+ * @package    core_grades
+ * @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 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
+            ),
+            '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
+     * @throws \dml_exception
+     * @throws \invalid_parameter_exception
+     * @throws \restricted_context_exception
+     * @throws coding_exception
+     * @throws moodle_exception
+     * @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 (!$gradeitem->is_using_direct_grading()) {
+            throw new moodle_exception("The {$itemname} item in {$component}/{$contextid} is not configured for direct grading");
+        }
+
+        // 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($grade);
+    }
+
+    /**
+     * 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/classes/grades/grader/gradingpanel/scale/external/fetch.php b/grade/classes/grades/grader/gradingpanel/scale/external/fetch.php
new file mode 100644 (file)
index 0000000..dfd9acd
--- /dev/null
@@ -0,0 +1,192 @@
+<?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 service functions relating to scale grades and grading.
+ *
+ * @package    core_grades
+ * @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 core_grades\grades\grader\gradingpanel\scale\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_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use required_capability_exception;
+use stdClass;
+
+/**
+ * External grading panel scale API
+ *
+ * @package    core_grades
+ * @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
+     * @throws \dml_exception
+     * @throws \invalid_parameter_exception
+     * @throws \restricted_context_exception
+     * @throws coding_exception
+     * @throws moodle_exception
+     * @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 (!$gradeitem->is_using_scale()) {
+            throw new moodle_exception("The {$itemname} item in {$component}/{$contextid} is not configured for grading with scales");
+        }
+
+        $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);
+        $currentgrade = (int) unformat_float($grade->grade);
+
+        $menu = $gradeitem->get_grade_menu();
+        $values = array_map(function($description, $value) use ($currentgrade) {
+            return [
+                'value' => $value,
+                'title' => $description,
+                'selected' => ($value == $currentgrade),
+            ];
+        }, $menu, array_keys($menu));
+
+        return [
+            'templatename' => 'core_grades/grades/grader/gradingpanel/scale',
+            'grade' => [
+                'options' => $values,
+                '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([
+                'options' => new external_multiple_structure(
+                    new external_single_structure([
+                        'value' => new external_value(PARAM_FLOAT, 'The grade value'),
+                        'title' => new external_value(PARAM_RAW, 'The description fo the option'),
+                        'selected' => new external_value(PARAM_BOOL, 'Whether this item is currently selected'),
+                    ]),
+                    'The description of the grade option'
+                ),
+                '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(),
+        ]);
+    }
+}
diff --git a/grade/classes/grades/grader/gradingpanel/scale/external/store.php b/grade/classes/grades/grader/gradingpanel/scale/external/store.php
new file mode 100644 (file)
index 0000000..6c7fa5b
--- /dev/null
@@ -0,0 +1,167 @@
+<?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 service functions relating to scale grades and grading.
+ *
+ * @package    core_grades
+ * @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 core_grades\grades\grader\gradingpanel\scale\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_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use required_capability_exception;
+
+/**
+ * External grading panel scale API
+ *
+ * @package    core_grades
+ * @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 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
+            ),
+            '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
+     * @throws \dml_exception
+     * @throws \invalid_parameter_exception
+     * @throws \restricted_context_exception
+     * @throws coding_exception
+     * @throws moodle_exception
+     * @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 (!$gradeitem->is_using_scale()) {
+            throw new moodle_exception("The {$itemname} item in {$component}/{$contextid} is not configured for grading with scales");
+        }
+
+        // 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/classes/local/gradeitem/advancedgrading_mapping.php b/grade/classes/local/gradeitem/advancedgrading_mapping.php
new file mode 100644 (file)
index 0000000..104ccfb
--- /dev/null
@@ -0,0 +1,43 @@
+<?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/>.
+
+/**
+ * Grade item, itemnumber mapping.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace core_grades\local\gradeitem;
+
+/**
+ * Grade item, itemnumber mapping.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ */
+interface advancedgrading_mapping {
+
+    /**
+     * Get the list of advanced grading item names for this component.
+     *
+     * @return array
+     */
+    public static function get_advancedgrading_itemnames(): array;
+}
diff --git a/grade/classes/local/gradeitem/itemnumber_mapping.php b/grade/classes/local/gradeitem/itemnumber_mapping.php
new file mode 100644 (file)
index 0000000..5db9726
--- /dev/null
@@ -0,0 +1,43 @@
+<?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/>.
+
+/**
+ * Grade item, itemnumber mapping.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace core_grades\local\gradeitem;
+
+/**
+ * Grade item, itemnumber mapping.
+ *
+ * @package   core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ */
+interface itemnumber_mapping {
+
+    /**
+     * Get the grade item mapping of item number to item name.
+     *
+     * @return array
+     */
+    public static function get_itemname_mapping_for_component(): array;
+}
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..4ef79c5
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..93c1615
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..de22d4d
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..5f60005
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/build/grades/grader/gradingpanel/comments/selectors.min.js b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments/selectors.min.js
new file mode 100644 (file)
index 0000000..d325a53
Binary files /dev/null and b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments/selectors.min.js differ
diff --git a/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments/selectors.min.js.map b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments/selectors.min.js.map
new file mode 100644 (file)
index 0000000..539a41d
Binary files /dev/null and b/grade/grading/form/guide/amd/build/grades/grader/gradingpanel/comments/selectors.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..5f08608
--- /dev/null
@@ -0,0 +1,77 @@
+// 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';
+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';
+
+/**
+ * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade.
+ *
+ * @param {String} component
+ * @param {Number} contextid
+ * @param {String} itemname
+ * @param {Number} gradeduserid
+ *
+ * @returns {Promise}
+ */
+export const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {
+    return fetchMany([{
+        methodname: `gradingform_guide_grader_gradingpanel_fetch`,
+        args: {
+            component,
+            contextid,
+            itemname,
+            gradeduserid,
+        },
+    }])[0];
+};
+
+/**
+ * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form.
+ *
+ * @param {String} component
+ * @param {Number} contextid
+ * @param {String} itemname
+ * @param {Number} gradeduserid
+ * @param {HTMLElement} rootNode
+ *
+ * @returns {Promise}
+ */
+export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, rootNode) => {
+    const form = rootNode.querySelector('form');
+
+    return normaliseResult(await 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..c2cbfb5
--- /dev/null
@@ -0,0 +1,57 @@
+// 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
+ */
+
+import Selectors from './comments/selectors';
+
+/**
+ * Manage the frequently used comments in the Marking Guide form.
+ *
+ * @param {String} rootId
+ */
+export const init = (rootId) => {
+    const rootNode = document.querySelector(`#${rootId}`);
+
+    rootNode.addEventListener('click', (e) => {
+        if (!e.target.matches(Selectors.frequentComment)) {
+            return;
+        }
+
+        e.preventDefault();
+
+        const clicked = e.target.closest(Selectors.frequentComment);
+        const criterion = clicked.closest(Selectors.criterion);
+        const remark = criterion.querySelector(Selectors.remark);
+
+        if (!remark) {
+            return;
+        }
+
+        // Either append the comment to an existing comment or set it as the comment.
+        if (remark.value.trim()) {
+            remark.value += `\n${clicked.innerHTML}`;
+        } else {
+            remark.value += clicked.innerHTML;
+        }
+    });
+};
diff --git a/grade/grading/form/guide/amd/src/grades/grader/gradingpanel/comments/selectors.js b/grade/grading/form/guide/amd/src/grades/grader/gradingpanel/comments/selectors.js
new file mode 100644 (file)
index 0000000..45bd89f
--- /dev/null
@@ -0,0 +1,28 @@
+// 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/>.
+
+/**
+ * Define all of the selectors we will be using on the Marking Guide interface.
+ *
+ * @module     gradingform_guide/grades/grader/gradingpanel/comments/selectors
+ * @package    gradingform_guide
+ * @copyright  2019 Mathew May <mathew.solutions>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+export default {
+    frequentComment: '[data-gradingform_guide-role="frequent-comment"]',
+    criterion: '[data-gradingform-guide-role="criterion"]',
+    remark: '[data-gradingform-guide-role="remark"]',
+};
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..fe34182
--- /dev/null
@@ -0,0 +1,292 @@
+<?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;
+
+global $CFG;
+
+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;
+require_once($CFG->dirroot.'/grade/grading/form/guide/lib.php');
+
+/**
+ * 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
+     * @throws \dml_exception
+     * @throws \invalid_parameter_exception
+     * @throws \restricted_context_exception
+     * @throws coding_exception
+     * @throws moodle_exception
+     * @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 (MARKING_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 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_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..f1300e0
--- /dev/null
@@ -0,0 +1,169 @@
+<?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;
+
+global $CFG;
+
+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;
+require_once($CFG->dirroot.'/grade/grading/form/guide/lib.php');
+
+/**
+ * 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
+     * @param string $formdata
+     * @return array
+     * @throws \dml_exception
+     * @throws \invalid_parameter_exception
+     * @throws \restricted_context_exception
+     * @throws coding_exception
+     * @throws moodle_exception
+     * @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 (MARKING_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..88ea5fd
--- /dev/null
@@ -0,0 +1,43 @@
+<?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    gradingform_guide
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$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..f878c5e 100644 (file)
@@ -26,6 +26,9 @@ defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->dirroot.'/grade/grading/form/lib.php');
 
+/** guide: Used to compare our gradeitem_type against. */
+const MARKING_GUIDE = 'guide';
+
 /**
  * This controller encapsulates the guide grading logic
  *
@@ -997,3 +1000,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..ff46c87 100644 (file)
     max-height: 80vh;
     overflow-y: auto;
 }
+.gradingform_guide-frequent-comments {
+    position: absolute;
+    top: 7px;
+    right: 0;
+}
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..7d816e6
--- /dev/null
@@ -0,0 +1,134 @@
+{{!
+    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:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * instanceid: Instance of the module this grading form belongs too
+    * criterion: A gradeable item in the Marking Guide
+    * name: Name of the gradeable item
+    * id: ID of the gradeable item
+    * description: Description shown to students for this gradeable item
+    * descriptionmarkers: Description shown to teachers for this gradeable item
+    * maxscore: Max allowable assinable points for this item
+    * score: Current score assigned to the learner for this item
+    * remark: Text input for the teacher to relay to the student
+    * hascomments: Flag for frequently used comments
+    * comments: Array of frequently used comments
+    * description: Description of a frequently used comment
+
+    Example context (json):
+    {
+        "instanceid": "42",
+        "criterion": [
+            {
+                "name": "Motivation",
+                "id": 13,
+                "description": "Show your motivation to rock climbing",
+                "descriptionmarkers": "Does the student show interest in climbing?",
+                "maxscore": 37,
+                "score": 20,
+                "remark": "That's great!",
+                "hascomments": true,
+                "comments": [
+                    {"description": "Great work!"},
+                    {"description": "You should really try it before jumping to conclusions"}
+                ]
+            }
+        ]
+    }
+}}
+<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}}
diff --git a/grade/grading/form/guide/tests/coverage.php b/grade/grading/form/guide/tests/coverage.php
new file mode 100644 (file)
index 0000000..84ed112
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+
+/**
+ * Coverage information for the gradingform_guide plugin.
+ *
+ * @package    gradingform_guide
+ * @category   test
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Coverage information for the gradingform_guide plugin.
+ *
+ * @package    gradingform_guide
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+return new class extends phpunit_coverage_info {
+    // Array The list of folders relative to the plugin root to whitelist in coverage generation.
+    protected $whitelistfolders = [
+        'classes',
+        'tests/generator',
+    ];
+};
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..bee39c5
--- /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   gradingform_guide
+ * @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   gradingform_guide
+ * @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..c55badf
--- /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   gradingform_guide
+ * @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   gradingform_guide
+ * @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;
index 2f06662..9c94f0d 100644 (file)
@@ -521,13 +521,53 @@ abstract class gradingform_controller {
      * @param int $raterid
      * @param int $itemid
      * @return gradingform_instance
+     * @throws dml_exception
      */
     public function get_or_create_instance($instanceid, $raterid, $itemid) {
+        if (!is_numeric($instanceid)) {
+            $instanceid = null;
+        }
+        return $this->fetch_instance($raterid, $itemid, $instanceid);
+    }
+
+    /**
+     * If an instanceid is specified and grading instance exists and it is created by this rater for
+     * this item, then the instance is returned.
+     *
+     * If instanceid is not known, then null can be passed to fetch the current instance matchign the specified raterid
+     * and itemid.
+     *
+     * If the instanceid is falsey, or no instance was found, then create a new instance for the specified rater and item.
+     *
+     * @param int $raterid
+     * @param int $itemid
+     * @param int $instanceid
+     * @return gradingform_instance
+     * @throws dml_exception
+     */
+    public function fetch_instance(int $raterid, int $itemid, ?int $instanceid): gradingform_instance {
         global $DB;
-        if ($instanceid &&
-                $instance = $DB->get_record('grading_instances', array('id'  => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) {
-            return $this->get_instance($instance);
+
+        $instance = null;
+        if (null === $instanceid) {
+            if ($instance = $this->get_current_instance($raterid, $itemid)) {
+                return $instance;
+            }
+            $instanceid = $instancerecord->id ?? null;
+        }
+
+        if (!empty($instanceid)) {
+            $instance = $DB->get_record('grading_instances', [
+                'id'  => $instanceid,
+                'raterid' => $raterid,
+                'itemid' => $itemid,
+            ], '*', IGNORE_MISSING);
+
+            if ($instance) {
+                return $this->get_instance($instance);
+            }
         }
+
         return $this->create_instance($raterid, $itemid);
     }
 
diff --git a/grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js b/grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js
new file mode 100644 (file)
index 0000000..b3a2eea
Binary files /dev/null and b/grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js differ
diff --git a/grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js.map b/grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js.map
new file mode 100644 (file)
index 0000000..8611812
Binary files /dev/null and b/grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js.map differ
diff --git a/grade/grading/form/rubric/amd/src/grades/grader/gradingpanel.js b/grade/grading/form/rubric/amd/src/grades/grader/gradingpanel.js
new file mode 100644 (file)
index 0000000..52307f5
--- /dev/null
@@ -0,0 +1,78 @@
+// 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';
+
+/**
+ * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade.
+ *
+ * @param {String} component
+ * @param {Number} contextid
+ * @param {String} itemname
+ * @param {Number} gradeduserid
+ *
+ * @returns {Promise}
+ */
+export const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {
+    return fetchMany([{
+        methodname: `gradingform_rubric_grader_gradingpanel_fetch`,
+        args: {
+            component,
+            contextid,
+            itemname,
+            gradeduserid,
+        },
+    }])[0];
+};
+
+/**
+ * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form.
+ *
+ * @param {String} component
+ * @param {Number} contextid
+ * @param {String} itemname
+ * @param {Number} gradeduserid
+ * @param {HTMLElement} rootNode
+ *
+ * @returns {Promise}
+ */
+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]);
+};
diff --git a/grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/fetch.php b/grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/fetch.php
new file mode 100644 (file)
index 0000000..c60f421
--- /dev/null
@@ -0,0 +1,298 @@
+<?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;
+
+global $CFG;
+
+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;
+require_once($CFG->dirroot.'/grade/grading/form/rubric/lib.php');
+
+/**
+ * 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 {
+        [
+            '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 and create the structure ready for Mustache.
+     *
+     * @param gradeitem $gradeitem
+     * @param stdClass $gradeduser
+     * @return array
+     */
+    public static function get_fetch_data(gradeitem $gradeitem, stdClass $gradeduser): array {
+        global $USER;
+
+        // Set up all the controllers etc that we'll be needing.
+        $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) {
+            // Iterate over the defined criterion in the rubric and map out what we need to render each item.
+            $criterion = array_map(function($criterion) use ($definitionid, $fillings, $context) {
+                // The general structure we'll be returning, we still need to get the remark (if any) and the levels associated.
+                $result = [
+                    'id' => $criterion['id'],
+                    'description' => self::get_formatted_text(
+                        $context,
+                        $definitionid,
+                        'description',
+                        $criterion['description'],
+                        (int) $criterion['descriptionformat']
+                    ),
+                ];
+
+                // Do we have an existing grade filling? if so lets get the remark associated to this criteria.
+                $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']
+                    );
+                }
+
+                // Lets build the levels within a criteria and figure out what needs to go where.
+                $result['levels'] = array_map(function($level) use ($criterion, $filling, $context, $definitionid) {
+                    // The bulk of what'll be returned can be defined easily we'll add to this further down.
+                    $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,
+                    ];
+
+                    // Consult the grade filling to see if a level has been selected and if it is the current level.
+                    if (array_key_exists('levelid', $filling) && $filling['levelid'] == $level['id']) {
+                        $result['checked'] = true;
+                    }
+
+                    return $result;
+                }, $criterion['levels']);
+
+                $nulllevel = [
+                    'id' => null,
+                    'criterionid' => $criterion['id'],
+                    'score' => '-',
+                    'definition' => 'Not set',
+                    'checked' => null,
+                ];
+                // Consult the grade filling to see if a level has been selected and if it is the current level.
+                if (array_key_exists('levelid', $filling) && $filling['levelid'] == 0) {
+                    $nulllevel['checked'] = true;
+                }
+
+                array_unshift($result['levels'], $nulllevel);
+
+                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_RAW, '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;
+    }
+}
diff --git a/grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/store.php b/grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/store.php
new file mode 100644 (file)
index 0000000..67ccfc7
--- /dev/null
@@ -0,0 +1,166 @@
+<?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;
+
+global $CFG;
+
+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;
+require_once($CFG->dirroot.'/grade/grading/form/rubric/lib.php');
+
+/**
+ * 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();
+    }
+}
diff --git a/grade/grading/form/rubric/db/services.php b/grade/grading/form/rubric/db/services.php
new file mode 100644 (file)
index 0000000..08b90a9
--- /dev/null
@@ -0,0 +1,45 @@
+<?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
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$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,
+    ],
+];
+
+
index d5951ae..6e2d299 100644 (file)
@@ -58,6 +58,7 @@ $string['lockzeropoints_help'] = 'This setting only applies if the sum of the mi
 $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.';
index 39af910..361e73b 100644 (file)
 defined('MOODLE_INTERNAL') || die();
 
 require_once($CFG->dirroot.'/grade/grading/form/lib.php');
+require_once($CFG->dirroot.'/lib/filelib.php');
+
+/** rubric: Used to compare our gradeitem_type against. */
+const RUBRIC = 'rubric';
 
 /**
  * This controller encapsulates the rubric grading logic
index 5dbbfec..3b9a2d2 100644 (file)
     position: relative;
     float: right;
 }
+
+.gradingpanel-gradingform_rubric [aria-checked="true"] {
+    border: 1px solid black;
+}
diff --git a/grade/grading/form/rubric/templates/grades/grader/gradingpanel.mustache b/grade/grading/form/rubric/templates/grades/grader/gradingpanel.mustache
new file mode 100644 (file)
index 0000000..94845f1
--- /dev/null
@@ -0,0 +1,105 @@
+{{!
+    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:
+    * none
+
+    Data attributes required for JS:
+    * none
+
+    Context variables required for this template:
+    * instanceid: Instance of the module this grading form belongs too
+    * criteria: A gradeable item in the Marking Guide
+    * id: The ID of the criteria
+    * description: Description of the criteria
+    * levels: The level that a criteria can be graded at
+    * criterionid: The ID of the criteria
+    * checked: Flag for if this is the currently selected level
+    * definition: Definition of the level
+    * remark: Text input for the teacher to relay to the student
+
+    Example context (json):
+    {
+        "instanceid": "42",
+        "criteria": [
+            {
+                "id": 13,
+                "description": "Show your motivation to rock climbing",
+                "levels": [
+                    {
+                        "criterionid": 13,
+                        "checked": true,
+                        "definition": "Great work!"
+                    }
+                ],
+                "remark": "That's great!"
+            }
+        ]
+    }
+}}
+<form id="gradingform_rubric-{{uniqid}}">
+    <input type="hidden" name="instanceid" value="{{instanceid}}">
+    <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>
diff --git a/grade/grading/form/rubric/tests/coverage.php b/grade/grading/form/rubric/tests/coverage.php
new file mode 100644 (file)
index 0000000..31da912
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+
+/**
+ * Coverage information for the gradingform_rubric plugin.
+ *
+ * @package    gradingform_rubric
+ * @category   test
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Coverage information for the gradingform_rubric plugin.
+ *
+ * @package    gradingform_rubric
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+return new class extends phpunit_coverage_info {
+    // Array The list of folders relative to the plugin root to whitelist in coverage generation.
+    protected $whitelistfolders = [
+        'classes',
+        'tests/generator',
+    ];
+};
diff --git a/grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_fetch_test.php b/grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_fetch_test.php
new file mode 100644 (file)
index 0000000..ec1ac94
--- /dev/null
@@ -0,0 +1,316 @@
+<?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   gradingform_rubric
+ * @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   gradingform_rubric
+ * @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'];
+                if (!isset($levelid)) {
+                    continue;
+                }
+                $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'];
+                if (!isset($levelid)) {
+                    continue;
+                }
+                $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'][1]['checked']);
+        $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']);
+        $this->assertEquals(1, $criteria[1]['levels'][3]['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,
+        ];
+    }
+}
diff --git a/grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_store_test.php b/grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_store_test.php
new file mode 100644 (file)
index 0000000..67954a4
--- /dev/null
@@ -0,0 +1,261 @@
+<?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   gradingform_rubric
+ * @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   gradingform_rubric
+ * @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);
+        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'];
+                if (!isset($levelid)) {
+                    continue;
+                }
+                $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'][1]['checked']);
+        $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']);
+        $this->assertEquals(1, $criteria[1]['levels'][3]['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,
+        ];
+    }
+}
index 0b35f3d..f08c267 100644 (file)
@@ -25,7 +25,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $plugin->component  = 'gradingform_rubric';
-$plugin->version    = 2019052000;
+$plugin->version    = 2019052006;
 
 $plugin->requires   = 2019051100;
 
index d2f57e9..d495448 100644 (file)
@@ -24,6 +24,8 @@
 
 defined('MOODLE_INTERNAL') || die();
 
+use core_grades\component_gradeitems;
+
 /**
  * Factory method returning an instance of the grading manager
  *
@@ -288,14 +290,29 @@ class grading_manager {
     public static function available_areas($component) {
         global $CFG;
 
+        if (component_gradeitems::defines_advancedgrading_itemnames_for_component($component)) {
+            $result = [];
+            foreach (component_gradeitems::get_advancedgrading_itemnames_for_component($component) as $itemnumber => $itemname) {
+                $result[$itemname] = get_string("gradeitem:{$itemname}", $component);
+            }
+
+            return $result;
+        }
+
         list($plugintype, $pluginname) = core_component::normalize_component($component);
 
         if ($component === 'core_grading') {
             return array();
 
         } else if ($plugintype === 'mod') {
-            return plugin_callback('mod', $pluginname, 'grading', 'areas_list', null, array());
-
+            $callbackfunction = "grading_areas_list";
+            if (component_callback_exists($component, $callbackfunction)) {
+                debugging(
+                    "Components supporting advanced grading should be updated to implement the component_gradeitems class",
+                    DEBUG_DEVELOPER
+                );
+                return component_callback($component, $callbackfunction, [], []);
+            }
         } else {
             throw new coding_exception('Unsupported area location');
         }
diff --git a/grade/grading/tests/coverage.php b/grade/grading/tests/coverage.php
new file mode 100644 (file)
index 0000000..9b2b565
--- /dev/null
@@ -0,0 +1,41 @@
+<?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/>.
+
+/**
+ * Coverage information for the core_grading subsystem.
+ *
+ * @package    core_grading
+ * @category   test
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Coverage information for the core_grading subsystem.
+ *
+ * @package    core_grading
+ * @copyright  2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+return new class extends phpunit_coverage_info {
+    // Array The list of folders relative to the plugin root to whitelist in coverage generation.
+    protected $whitelistfolders = [
+        'classes',
+        'tests/generator',
+    ];
+};
diff --git a/grade/templates/grades/grader/gradingpanel/point.mustache b/grade/templates/grades/grader/gradingpanel/point.mustache
new file mode 100644 (file)
index 0000000..ebf77c3
--- /dev/null
@@ -0,0 +1,35 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_grades/grades/grader/gradingpanel/point
+
+    Point-based grading template for use in the grading panel.
+
+    Context variables required for this template:
+
+    Example context (json):
+    {
+      "grade": 47
+    }
+}}
+<form>
+  <div class="form-group">
+    <label for="core_grades-grade-{{uniqid}}">{{#str}}grade, moodle{{/str}}</label>
+    <input class="form-control" type="number" name="grade" value="{{grade}}" id="core_grades-grade-{{uniqid}}" aria-describedby="core_grades-help-{{uniqid}}">
+    <small id="core_grades-help-{{uniqid}}" class="form-text text-muted">{{#str}}grade_help, core_grades{{/str}}</small>
+  </div>
+</form>
diff --git a/grade/templates/grades/grader/gradingpanel/scale.mustache b/grade/templates/grades/grader/gradingpanel/scale.mustache
new file mode 100644 (file)
index 0000000..fdac05d
--- /dev/null
@@ -0,0 +1,42 @@
+{{!
+    This file is part of Moodle - http://moodle.org/
+
+    Moodle is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Moodle is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+}}
+{{!
+    @template core_grades/grades/grader/gradingpanel/scale
+
+    Scale-based grading template for use in the grading panel.
+
+    Context variables required for this template:
+
+    Example context (json):
+    {
+        "value": 1,
+        "selected": true,
+        "title": "Motivational"
+    }
+}}
+<form>
+  <div class="form-group">
+    <label for="core_grades-grade-{{uniqid}}">{{#str}}grade, moodle{{/str}}</label>
+    <select class="form-control" name="grade" id="core_grades-grade-{{uniqid}}" aria-describedby="core_grades-help-{{uniqid}}">
+        <option value="-1">{{#str}} nograde, moodle{{/str}}</option>
+    {{#options}}
+        <option value="{{value}}" {{#selected}}selected{{/selected}}>{{title}}</option>
+    {{/options}}
+    </select>
+    <small id="core_grades-help-{{uniqid}}" class="form-text text-muted">{{#str}}grade_help, core_grades{{/str}}</small>
+  </div>
+</form>