From eb9df053eba0cb3ef5c79292d594efb22ec9421e Mon Sep 17 00:00:00 2001 From: Mathew May Date: Thu, 12 Sep 2019 14:44:21 +0800 Subject: [PATCH] MDL-66697 gradingform_rubric: Support new grading panel Part of MDL-66074 --- .../build/grades/grader/gradingpanel.min.js | 2 + .../grades/grader/gradingpanel.min.js.map | 1 + .../amd/src/grades/grader/gradingpanel.js | 58 ++++ .../grader/gradingpanel/external/fetch.php | 276 ++++++++++++++++ .../grader/gradingpanel/external/store.php | 163 +++++++++ grade/grading/form/rubric/db/services.php | 43 +++ .../rubric/lang/en/gradingform_rubric.php | 1 + grade/grading/form/rubric/lib.php | 1 + grade/grading/form/rubric/styles.css | 4 + .../grades/grader/gradingpanel.mustache | 83 +++++ ...radingpanel_rubric_external_fetch_test.php | 310 ++++++++++++++++++ ...radingpanel_rubric_external_store_test.php | 259 +++++++++++++++ grade/grading/form/rubric/version.php | 2 +- mod/forum/amd/build/grades/grader.min.js.map | 2 +- mod/forum/amd/src/grades/grader.js | 3 +- .../grades/local/grader/grading.mustache | 4 +- theme/boost/scss/moodle/grade.scss | 16 + theme/boost/style/moodle.css | 24 ++ theme/classic/style/moodle.css | 24 ++ 19 files changed, 1270 insertions(+), 6 deletions(-) create mode 100644 grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js create mode 100644 grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js.map create mode 100644 grade/grading/form/rubric/amd/src/grades/grader/gradingpanel.js create mode 100644 grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/fetch.php create mode 100644 grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/store.php create mode 100644 grade/grading/form/rubric/db/services.php create mode 100644 grade/grading/form/rubric/templates/grades/grader/gradingpanel.mustache create mode 100644 grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_fetch_test.php create mode 100644 grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_store_test.php 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 index 00000000000..b3a2eeae770 --- /dev/null +++ b/grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js @@ -0,0 +1,2 @@ +define ("gradingform_rubric/grades/grader/gradingpanel",["exports","core/ajax","core_grades/grades/grader/gradingpanel/normalise","jquery"],function(a,b,c,d){"use strict";Object.defineProperty(a,"__esModule",{value:!0});a.storeCurrentGrade=a.fetchCurrentGrade=void 0;d=function(a){return a&&a.__esModule?a:{default:a}}(d);function e(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){c(a);return}if(h.done){b(i)}else{Promise.resolve(i).then(d,e)}}function f(a){return function(){var b=this,c=arguments;return new Promise(function(d,f){var i=a.apply(b,c);function g(a){e(i,d,f,g,h,"next",a)}function h(a){e(i,d,f,g,h,"throw",a)}g(void 0)})}}a.fetchCurrentGrade=function fetchCurrentGrade(a,c,d,e){return(0,b.call)([{methodname:"gradingform_rubric_grader_gradingpanel_fetch",args:{component:a,contextid:c,itemname:d,gradeduserid:e}}])[0]};var g=function(){var a=f(regeneratorRuntime.mark(function a(e,f,g,h,i){var j;return regeneratorRuntime.wrap(function(a){while(1){switch(a.prev=a.next){case 0:j=i.querySelector("form");a.t0=c.normaliseResult;a.next=4;return(0,b.call)([{methodname:"gradingform_rubric_grader_gradingpanel_store",args:{component:e,contextid:f,itemname:g,gradeduserid:h,formdata:(0,d.default)(j).serialize()}}])[0];case 4:a.t1=a.sent;return a.abrupt("return",(0,a.t0)(a.t1));case 6:case"end":return a.stop();}}},a)}));return function(){return a.apply(this,arguments)}}();a.storeCurrentGrade=g}); +//# sourceMappingURL=gradingpanel.min.js.map 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 index 00000000000..0723bdbb8f5 --- /dev/null +++ b/grade/grading/form/rubric/amd/build/grades/grader/gradingpanel.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/grades/grader/gradingpanel.js"],"names":["fetchCurrentGrade","component","contextid","itemname","gradeduserid","methodname","args","storeCurrentGrade","rootNode","form","querySelector","normaliseResult","formdata","serialize"],"mappings":"2QA6BA,uD,mVAEiC,QAApBA,CAAAA,iBAAoB,CAACC,CAAD,CAAYC,CAAZ,CAAuBC,CAAvB,CAAiCC,CAAjC,CAAkD,CAC/E,MAAO,WAAU,CAAC,CACdC,UAAU,+CADI,CAEdC,IAAI,CAAE,CACFL,SAAS,CAATA,CADE,CAEFC,SAAS,CAATA,CAFE,CAGFC,QAAQ,CAARA,CAHE,CAIFC,YAAY,CAAZA,CAJE,CAFQ,CAAD,CAAV,EAQH,CARG,CASV,C,CAGM,GAAMG,CAAAA,CAAiB,4CAAG,WAAMN,CAAN,CAAiBC,CAAjB,CAA4BC,CAA5B,CAAsCC,CAAtC,CAAoDI,CAApD,yFACvBC,CADuB,CAChBD,CAAQ,CAACE,aAAT,CAAuB,MAAvB,CADgB,MAGtBC,iBAHsB,gBAGA,WAAU,CAAC,CACpCN,UAAU,+CAD0B,CAEpCC,IAAI,CAAE,CACFL,SAAS,CAATA,CADE,CAEFC,SAAS,CAATA,CAFE,CAGFC,QAAQ,CAARA,CAHE,CAIFC,YAAY,CAAZA,CAJE,CAKFQ,QAAQ,CAAE,cAAOH,CAAP,EAAaI,SAAb,EALR,CAF8B,CAAD,CAAV,EASzB,CATyB,CAHA,qGAAH,uDAAvB,C","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Grading panel for gradingform_rubric.\n *\n * @module gradingform_rubric/grades/grader/gradingpanel\n * @package gradingform_rubric\n * @copyright 2019 Mathew May \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {call as fetchMany} from 'core/ajax';\nimport {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise';\n\n\n// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()\nimport jQuery from 'jquery';\n\nexport const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {\n return fetchMany([{\n methodname: `gradingform_rubric_grader_gradingpanel_fetch`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n },\n }])[0];\n};\n\n\nexport const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, rootNode) => {\n const form = rootNode.querySelector('form');\n\n return normaliseResult(await fetchMany([{\n methodname: `gradingform_rubric_grader_gradingpanel_store`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n formdata: jQuery(form).serialize(),\n },\n }])[0]);\n};\n"],"file":"gradingpanel.min.js"} \ No newline at end of file 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 index 00000000000..90885a902b2 --- /dev/null +++ b/grade/grading/form/rubric/amd/src/grades/grader/gradingpanel.js @@ -0,0 +1,58 @@ +// 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 . + +/** + * Grading panel for gradingform_rubric. + * + * @module gradingform_rubric/grades/grader/gradingpanel + * @package gradingform_rubric + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {call as fetchMany} from 'core/ajax'; +import {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise'; + + +// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send() +import jQuery from 'jquery'; + +export const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => { + return fetchMany([{ + methodname: `gradingform_rubric_grader_gradingpanel_fetch`, + args: { + component, + contextid, + itemname, + gradeduserid, + }, + }])[0]; +}; + + +export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, rootNode) => { + const form = rootNode.querySelector('form'); + + return normaliseResult(await fetchMany([{ + methodname: `gradingform_rubric_grader_gradingpanel_store`, + args: { + component, + contextid, + itemname, + gradeduserid, + formdata: jQuery(form).serialize(), + }, + }])[0]); +}; 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 index 00000000000..1a9b7e5f296 --- /dev/null +++ b/grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/fetch.php @@ -0,0 +1,276 @@ +. + +/** + * Web services relating to fetching of a rubric for the grading panel. + * + * @package gradingform_rubric + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +declare(strict_types = 1); + +namespace gradingform_rubric\grades\grader\gradingpanel\external; + +use coding_exception; +use context; +use core_grades\component_gradeitem as gradeitem; +use core_grades\component_gradeitems; +use external_api; +use external_function_parameters; +use external_multiple_structure; +use external_single_structure; +use external_value; +use external_warnings; +use stdClass; +use moodle_exception; + +/** + * Web services relating to fetching of a rubric for the grading panel. + * + * @package gradingform_rubric + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class fetch extends external_api { + + /** + * Describes the parameters for fetching the grading panel for a simple grade. + * + * @return external_function_parameters + * @since Moodle 3.8 + */ + public static function execute_parameters(): external_function_parameters { + return new external_function_parameters ([ + 'component' => new external_value( + PARAM_ALPHANUMEXT, + 'The name of the component', + VALUE_REQUIRED + ), + 'contextid' => new external_value( + PARAM_INT, + 'The ID of the context being graded', + VALUE_REQUIRED + ), + 'itemname' => new external_value( + PARAM_ALPHANUM, + 'The grade item itemname being graded', + VALUE_REQUIRED + ), + 'gradeduserid' => new external_value( + PARAM_INT, + 'The ID of the user show', + VALUE_REQUIRED + ), + ]); + } + + /** + * Fetch the data required to build a grading panel for a simple grade. + * + * @param string $component + * @param int $contextid + * @param string $itemname + * @param int $gradeduserid + * @return array + * @since Moodle 3.8 + */ + public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid): array { + global $USER; + + [ + 'component' => $component, + 'contextid' => $contextid, + 'itemname' => $itemname, + 'gradeduserid' => $gradeduserid, + ] = self::validate_parameters(self::execute_parameters(), [ + 'component' => $component, + 'contextid' => $contextid, + 'itemname' => $itemname, + 'gradeduserid' => $gradeduserid, + ]); + + // Validate the context. + $context = context::instance_by_id($contextid); + self::validate_context($context); + + // Validate that the supplied itemname is a gradable item. + if (!component_gradeitems::is_valid_itemname($component, $itemname)) { + throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component"); + } + + // Fetch the gradeitem instance. + $gradeitem = gradeitem::instance($component, $context, $itemname); + + if ('rubric' !== $gradeitem->get_advanced_grading_method()) { + throw new moodle_exception( + "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a rubric" + ); + } + + // Fetch the actual data. + $gradeduser = \core_user::get_user($gradeduserid); + + return self::get_fetch_data($gradeitem, $gradeduser); + } + + /** + * Get the data to be fetched. + * + * @param gradeitem $gradeitem + * @param stdClass $gradeduser + * @return array + */ + public static function get_fetch_data(gradeitem $gradeitem, stdClass $gradeduser): array { + global $USER; + + $grade = $gradeitem->get_grade_for_user($gradeduser, $USER); + $instance = $gradeitem->get_advanced_grading_instance($USER, $grade); + $controller = $instance->get_controller(); + $definition = $controller->get_definition(); + $fillings = $instance->get_rubric_filling(); + $context = $controller->get_context(); + $definitionid = (int) $definition->id; + + $teacherdescription = self::get_formatted_text( + $context, + $definitionid, + 'description', + $definition->description, + (int) $definition->descriptionformat + ); + + $criterion = []; + if ($definition->rubric_criteria) { + $criterion = array_map(function($criterion) use ($definitionid, $fillings, $context) { + $result = [ + 'id' => $criterion['id'], + 'description' => self::get_formatted_text( + $context, + $definitionid, + 'description', + $criterion['description'], + (int) $criterion['descriptionformat'] + ), + ]; + + $filling = []; + if (array_key_exists($criterion['id'], $fillings['criteria'])) { + $filling = $fillings['criteria'][$criterion['id']]; + $result['remark'] = self::get_formatted_text($context, + $definitionid, + 'remark', + $filling['remark'], + (int) $filling['remarkformat'] + ); + } + + $result['levels'] = array_map(function($level) use ($criterion, $filling, $context, $definitionid) { + $result = [ + 'id' => $level['id'], + 'criterionid' => $criterion['id'], + 'score' => $level['score'], + 'definition' => self::get_formatted_text( + $context, + $definitionid, + 'definition', + $level['definition'], + (int) $level['definitionformat'] + ), + 'checked' => null, + ]; + + if (array_key_exists('levelid', $filling) && $filling['levelid'] == $level['id']) { + $result['checked'] = true; + } + + return $result; + }, $criterion['levels']); + + return $result; + }, $definition->rubric_criteria); + } + + return [ + 'templatename' => 'gradingform_rubric/grades/grader/gradingpanel', + 'grade' => [ + 'instanceid' => $instance->get_id(), + 'criteria' => $criterion, + 'rubricmode' => 'evaluate editable', + 'teacherdescription' => $teacherdescription, + 'canedit' => false, + 'timecreated' => $grade->timecreated, + 'timemodified' => $grade->timemodified, + ], + 'warnings' => [], + ]; + } + + /** + * Describes the data returned from the external function. + * + * @return external_single_structure + * @since Moodle 3.8 + */ + public static function execute_returns(): external_single_structure { + return new external_single_structure([ + 'templatename' => new external_value(PARAM_SAFEPATH, 'The template to use when rendering this data'), + 'grade' => new external_single_structure([ + 'instanceid' => new external_value(PARAM_INT, 'The id of the current grading instance'), + 'rubricmode' => new external_value(PARAM_RAW, 'The mode i.e. evaluate editable'), + 'canedit' => new external_value(PARAM_BOOL, 'Can the user edit this'), + 'criteria' => new external_multiple_structure( + new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'ID of the Criteria'), + 'description' => new external_value(PARAM_RAW, 'Description of the Criteria'), + 'remark' => new external_value(PARAM_RAW, 'Any remarks for this criterion for the user being assessed', VALUE_OPTIONAL), + 'levels' => new external_multiple_structure(new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'ID of level'), + 'criterionid' => new external_value(PARAM_INT, 'ID of the criterion this matches to'), + 'score' => new external_value(PARAM_INT, 'What this level is worth'), + 'definition' => new external_value(PARAM_RAW, 'Definition of the level'), + 'checked' => new external_value(PARAM_BOOL, 'Selected flag'), + ])), + ]) + ), + 'timecreated' => new external_value(PARAM_INT, 'The time that the grade was created'), + 'timemodified' => new external_value(PARAM_INT, 'The time that the grade was last updated'), + ]), + 'warnings' => new external_warnings(), + ]); + } + + /** + * Get a formatted version of the remark/description/etc. + * + * @param context $context + * @param int $definitionid + * @param string $filearea The file area of the field + * @param string $text The text to be formatted + * @param int $format The input format of the string + * @return string + */ + protected static function get_formatted_text(context $context, int $definitionid, string $filearea, string $text, int $format): string { + $formatoptions = [ + 'noclean' => false, + 'trusted' => false, + 'filter' => true, + ]; + [$newtext, ] = external_format_text($text, $format, $context, 'grading', $filearea, $definitionid, $formatoptions); + return $newtext; + } +} 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 index 00000000000..dca2d67d067 --- /dev/null +++ b/grade/grading/form/rubric/classes/grades/grader/gradingpanel/external/store.php @@ -0,0 +1,163 @@ +. + +/** + * Web services relating to fetching of a rubric for the grading panel. + * + * @package gradingform_rubric + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +declare(strict_types = 1); + +namespace gradingform_rubric\grades\grader\gradingpanel\external; + +use coding_exception; +use context; +use core_grades\component_gradeitem as gradeitem; +use core_grades\component_gradeitems; +use external_api; +use external_function_parameters; +use external_single_structure; +use external_value; +use moodle_exception; + +/** + * Web services relating to storing of a rubric for the grading panel. + * + * @package gradingform_rubric + * @copyright 2019 Mathew May + * @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 index 00000000000..e7488e6cd4f --- /dev/null +++ b/grade/grading/form/rubric/db/services.php @@ -0,0 +1,43 @@ +. + +/** + * Rubric external functions and service definitions. + * + * @package gradingform_rubric + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$functions = [ + 'gradingform_rubric_grader_gradingpanel_fetch' => [ + 'classname' => 'gradingform_rubric\\grades\\grader\\gradingpanel\\external\\fetch', + 'methodname' => 'execute', + 'description' => 'Fetch the data required to display the grader grading panel, ' . + 'creating the grade item if required', + 'type' => 'write', + 'ajax' => true, + ], + 'gradingform_rubric_grader_gradingpanel_store' => [ + 'classname' => 'gradingform_rubric\\grades\\grader\\gradingpanel\\external\\store', + 'methodname' => 'execute', + 'description' => 'Store the grading data for a user from the grader grading panel.', + 'type' => 'write', + 'ajax' => true, + ], +]; + + diff --git a/grade/grading/form/rubric/lang/en/gradingform_rubric.php b/grade/grading/form/rubric/lang/en/gradingform_rubric.php index d5951aec2f5..6e2d29973ff 100644 --- a/grade/grading/form/rubric/lang/en/gradingform_rubric.php +++ b/grade/grading/form/rubric/lang/en/gradingform_rubric.php @@ -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.'; diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php index 39af91091c7..e81c8ea544f 100644 --- a/grade/grading/form/rubric/lib.php +++ b/grade/grading/form/rubric/lib.php @@ -25,6 +25,7 @@ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/grade/grading/form/lib.php'); +require_once($CFG->dirroot.'/lib/filelib.php'); /** * This controller encapsulates the rubric grading logic diff --git a/grade/grading/form/rubric/styles.css b/grade/grading/form/rubric/styles.css index 5dbbfec7dfa..3b9a2d2fe5b 100644 --- a/grade/grading/form/rubric/styles.css +++ b/grade/grading/form/rubric/styles.css @@ -308,3 +308,7 @@ 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 index 00000000000..e437c8f39e2 --- /dev/null +++ b/grade/grading/form/rubric/templates/grades/grader/gradingpanel.mustache @@ -0,0 +1,83 @@ +{{! + 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 . +}} +{{! + @template gradingform_rubric/grades/grader/gradingpanel + + Classes required for JS: + * TODO + + Data attributes required for JS: + * TODO + + Context variables required for this template: + * TODO + + Example context (json): + { + } +}} +
+ +
{{{teacherdescription}}}
+
+ {{#criteria}} +
+
{{{description}}}
+ +
+
+ {{#levels}} +
+ + +
+ {{/levels}} +
+ + +
+
+ {{/criteria}} +
+
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 index 00000000000..cd4c0f51fa9 --- /dev/null +++ b/grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_fetch_test.php @@ -0,0 +1,310 @@ +. + +/** + * Unit tests for core_grades\component_gradeitems; + * + * @package core_grades + * @category test + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +declare(strict_types = 1); + +namespace gradingform_rubric\grades\grader\gradingpanel\external; + +use advanced_testcase; +use coding_exception; +use core_grades\component_gradeitem; +use external_api; +use mod_forum\local\entities\forum as forum_entity; +use moodle_exception; + +/** + * Unit tests for core_grades\component_gradeitems; + * + * @package core_grades + * @category test + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class fetch_test extends advanced_testcase { + + public static function setupBeforeClass(): void { + global $CFG; + require_once("{$CFG->libdir}/externallib.php"); + } + + /** + * Ensure that an execute with an invalid component is rejected. + */ + public function test_execute_invalid_component(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException(coding_exception::class); + $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component"); + fetch::execute('mod_invalid', 1, 'foo', 2); + } + + /** + * Ensure that an execute with an invalid itemname on a valid component is rejected. + */ + public function test_execute_invalid_itemname(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException(coding_exception::class); + $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component"); + fetch::execute('mod_forum', 1, 'foo', 2); + } + + /** + * Ensure that an execute against a different grading method is rejected. + */ + public function test_execute_incorrect_type(): void { + $this->resetAfterTest(); + + $forum = $this->get_forum_instance([ + // Negative numbers mean a scale. + 'grade_forum' => 5, + ]); + $course = $forum->get_course_record(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $this->setUser($teacher); + + $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); + + $this->expectException(moodle_exception::class); + $this->expectExceptionMessage("not configured for advanced grading with a rubric"); + fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id); + } + + /** + * Ensure that an execute against the correct grading method returns the current state of the user. + */ + public function test_execute_fetch_empty(): void { + $this->resetAfterTest(); + + [ + 'forum' => $forum, + 'controller' => $controller, + 'definition' => $definition, + 'student' => $student, + 'teacher' => $teacher, + ] = $this->get_test_data(); + + $this->setUser($teacher); + + $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); + + $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id); + $result = external_api::clean_returnvalue(fetch::execute_returns(), $result); + + $this->assertIsArray($result); + $this->assertArrayHasKey('templatename', $result); + + $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']); + + $this->assertArrayHasKey('grade', $result); + $this->assertIsArray($result['grade']); + + $this->assertIsInt($result['grade']['timecreated']); + $this->assertArrayHasKey('timemodified', $result['grade']); + $this->assertIsInt($result['grade']['timemodified']); + + $this->assertArrayHasKey('warnings', $result); + $this->assertIsArray($result['warnings']); + $this->assertEmpty($result['warnings']); + + $this->assertArrayHasKey('criteria', $result['grade']); + $criteria = $result['grade']['criteria']; + $this->assertCount(count($definition->rubric_criteria), $criteria); + foreach ($criteria as $criterion) { + $this->assertArrayHasKey('id', $criterion); + $criterionid = $criterion['id']; + $sourcecriterion = $definition->rubric_criteria[$criterionid]; + + $this->assertArrayHasKey('description', $criterion); + $this->assertEquals($sourcecriterion['description'], $criterion['description']); + + $this->assertArrayHasKey('levels', $criterion); + + $levels = $criterion['levels']; + foreach ($levels as $level) { + $levelid = $level['id']; + $sourcelevel = $sourcecriterion['levels'][$levelid]; + + $this->assertArrayHasKey('criterionid', $level); + $this->assertEquals($criterionid, $level['criterionid']); + + $this->assertArrayHasKey('checked', $level); + + $this->assertArrayHasKey('definition', $level); + $this->assertEquals($sourcelevel['definition'], $level['definition']); + + $this->assertArrayHasKey('score', $level); + $this->assertEquals($sourcelevel['score'], $level['score']); + } + } + } + + /** + * Ensure that an execute against the correct grading method returns the current state of the user. + */ + public function test_execute_fetch_graded(): void { + $this->resetAfterTest(); + $generator = \testing_util::get_data_generator(); + $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); + + [ + 'forum' => $forum, + 'controller' => $controller, + 'definition' => $definition, + 'student' => $student, + 'teacher' => $teacher, + ] = $this->get_test_data(); + + $this->setUser($teacher); + + $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); + $grade = $gradeitem->get_grade_for_user($student, $teacher); + $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade); + + $submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $student->id, + 0, 'Too many mistakes. Please try again.', + 2, 'Great number of pictures. Well done.' + ); + + $gradeitem->store_grade_from_formdata($student, $teacher, (object) [ + 'instanceid' => $instance->get_id(), + 'advancedgrading' => $submissiondata, + ]); + + $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id); + $result = external_api::clean_returnvalue(fetch::execute_returns(), $result); + + $this->assertIsArray($result); + $this->assertArrayHasKey('templatename', $result); + + $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']); + + $this->assertArrayHasKey('grade', $result); + $this->assertIsArray($result['grade']); + + $this->assertIsInt($result['grade']['timecreated']); + $this->assertArrayHasKey('timemodified', $result['grade']); + $this->assertIsInt($result['grade']['timemodified']); + + $this->assertArrayHasKey('warnings', $result); + $this->assertIsArray($result['warnings']); + $this->assertEmpty($result['warnings']); + + $this->assertArrayHasKey('criteria', $result['grade']); + $criteria = $result['grade']['criteria']; + $this->assertCount(count($definition->rubric_criteria), $criteria); + foreach ($criteria as $criterion) { + $this->assertArrayHasKey('id', $criterion); + $criterionid = $criterion['id']; + $sourcecriterion = $definition->rubric_criteria[$criterionid]; + + $this->assertArrayHasKey('description', $criterion); + $this->assertEquals($sourcecriterion['description'], $criterion['description']); + + $this->assertArrayHasKey('remark', $criterion); + + $this->assertArrayHasKey('levels', $criterion); + + $levels = $criterion['levels']; + foreach ($levels as $level) { + $levelid = $level['id']; + $sourcelevel = $sourcecriterion['levels'][$levelid]; + + $this->assertArrayHasKey('criterionid', $level); + $this->assertEquals($criterionid, $level['criterionid']); + + $this->assertArrayHasKey('checked', $level); + + $this->assertArrayHasKey('definition', $level); + $this->assertEquals($sourcelevel['definition'], $level['definition']); + + $this->assertArrayHasKey('score', $level); + $this->assertEquals($sourcelevel['score'], $level['score']); + } + + } + + $this->assertEquals(1, $criteria[0]['levels'][0]['checked']); + $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']); + $this->assertEquals(1, $criteria[1]['levels'][2]['checked']); + $this->assertEquals('Great number of pictures. Well done.', $criteria[1]['remark']); + } + + /** + * Get a forum instance. + * + * @param array $config + * @return forum_entity + */ + protected function get_forum_instance(array $config = []): forum_entity { + $this->resetAfterTest(); + + $datagenerator = $this->getDataGenerator(); + $course = $datagenerator->create_course(); + $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id])); + + $vaultfactory = \mod_forum\local\container::get_vault_factory(); + $vault = $vaultfactory->get_forum_vault(); + + return $vault->get_from_id((int) $forum->id); + } + + /** + * Get test data for forums graded using a rubric. + * + * @return array + */ + protected function get_test_data(): array { + global $DB; + + $this->resetAfterTest(); + + $generator = \testing_util::get_data_generator(); + $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); + + $forum = $this->get_forum_instance(); + $course = $forum->get_course_record(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $this->setUser($teacher); + $controller = $rubricgenerator->get_test_rubric($forum->get_context(), 'forum', 'forum'); + $definition = $controller->get_definition(); + + $DB->set_field('forum', 'grade_forum', count($definition->rubric_criteria), ['id' => $forum->get_id()]); + return [ + 'forum' => $forum, + 'controller' => $controller, + 'definition' => $definition, + 'student' => $student, + 'teacher' => $teacher, + ]; + } +} 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 index 00000000000..63356219748 --- /dev/null +++ b/grade/grading/form/rubric/tests/grades_grader_gradingpanel_rubric_external_store_test.php @@ -0,0 +1,259 @@ +. + +/** + * Unit tests for core_grades\component_gradeitems; + * + * @package core_grades + * @category test + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +declare(strict_types = 1); + +namespace gradingform_rubric\grades\grader\gradingpanel\external; + +use advanced_testcase; +use coding_exception; +use core_grades\component_gradeitem; +use external_api; +use mod_forum\local\entities\forum as forum_entity; +use moodle_exception; + +/** + * Unit tests for core_grades\component_gradeitems; + * + * @package core_grades + * @category test + * @copyright 2019 Mathew May + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class store_test extends advanced_testcase { + + public static function setupBeforeClass(): void { + global $CFG; + require_once("{$CFG->libdir}/externallib.php"); + } + + /** + * Ensure that an execute with an invalid component is rejected. + */ + public function test_execute_invalid_component(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException(coding_exception::class); + $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component"); + store::execute('mod_invalid', 1, 'foo', 2, 'formdata'); + } + + /** + * Ensure that an execute with an invalid itemname on a valid component is rejected. + */ + public function test_execute_invalid_itemname(): void { + $this->resetAfterTest(); + $user = $this->getDataGenerator()->create_user(); + $this->setUser($user); + + $this->expectException(coding_exception::class); + $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component"); + store::execute('mod_forum', 1, 'foo', 2, 'formdata'); + } + + /** + * Ensure that an execute against a different grading method is rejected. + */ + public function test_execute_incorrect_type(): void { + $this->resetAfterTest(); + + $forum = $this->get_forum_instance([ + 'grade_forum' => 5, + ]); + $course = $forum->get_course_record(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $this->setUser($teacher); + + $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); + + $this->expectException(moodle_exception::class); + //$this->expectExceptionMessage("not configured for advanced grading with a rubric"); + store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata'); + } + + /** + * Ensure that an execute against a different grading method is rejected. + */ + public function test_execute_disabled(): void { + $this->resetAfterTest(); + + $forum = $this->get_forum_instance(); + $course = $forum->get_course_record(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $this->setUser($teacher); + + $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); + + $this->expectException(moodle_exception::class); + $this->expectExceptionMessage("Grading is not enabled"); + store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata'); + } + + /** + * Ensure that an execute against the correct grading method returns the current state of the user. + */ + public function test_execute_store_graded(): void { + $this->resetAfterTest(); + $generator = \testing_util::get_data_generator(); + $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); + + [ + 'forum' => $forum, + 'controller' => $controller, + 'definition' => $definition, + 'student' => $student, + 'teacher' => $teacher, + ] = $this->get_test_data(); + + $this->setUser($teacher); + + $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum'); + $grade = $gradeitem->get_grade_for_user($student, $teacher); + $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade); + + $submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $student->id, + 0, 'Too many mistakes. Please try again.', + 2, 'Great number of pictures. Well done.' + ); + + $formdata = http_build_query((object) [ + 'instanceid' => $instance->get_id(), + 'advancedgrading' => $submissiondata, + ], '', '&'); + + $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, $formdata); + $result = external_api::clean_returnvalue(store::execute_returns(), $result); + + $this->assertIsArray($result); + $this->assertArrayHasKey('templatename', $result); + + $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']); + + $this->assertArrayHasKey('grade', $result); + $this->assertIsArray($result['grade']); + + $this->assertIsInt($result['grade']['timecreated']); + $this->assertArrayHasKey('timemodified', $result['grade']); + $this->assertIsInt($result['grade']['timemodified']); + + $this->assertArrayHasKey('warnings', $result); + $this->assertIsArray($result['warnings']); + $this->assertEmpty($result['warnings']); + + $this->assertArrayHasKey('criteria', $result['grade']); + $criteria = $result['grade']['criteria']; + $this->assertCount(count($definition->rubric_criteria), $criteria); + foreach ($criteria as $criterion) { + $this->assertArrayHasKey('id', $criterion); + $criterionid = $criterion['id']; + $sourcecriterion = $definition->rubric_criteria[$criterionid]; + + $this->assertArrayHasKey('description', $criterion); + $this->assertEquals($sourcecriterion['description'], $criterion['description']); + + $this->assertArrayHasKey('remark', $criterion); + + $this->assertArrayHasKey('levels', $criterion); + + $levels = $criterion['levels']; + foreach ($levels as $level) { + $levelid = $level['id']; + $sourcelevel = $sourcecriterion['levels'][$levelid]; + + $this->assertArrayHasKey('criterionid', $level); + $this->assertEquals($criterionid, $level['criterionid']); + + $this->assertArrayHasKey('checked', $level); + + $this->assertArrayHasKey('definition', $level); + $this->assertEquals($sourcelevel['definition'], $level['definition']); + + $this->assertArrayHasKey('score', $level); + $this->assertEquals($sourcelevel['score'], $level['score']); + } + + } + + $this->assertEquals(1, $criteria[0]['levels'][0]['checked']); + $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']); + $this->assertEquals(1, $criteria[1]['levels'][2]['checked']); + $this->assertEquals('Great number of pictures. Well done.', $criteria[1]['remark']); + } + + /** + * Get a forum instance. + * + * @param array $config + * @return forum_entity + */ + protected function get_forum_instance(array $config = []): forum_entity { + $this->resetAfterTest(); + + $datagenerator = $this->getDataGenerator(); + $course = $datagenerator->create_course(); + $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id])); + + $vaultfactory = \mod_forum\local\container::get_vault_factory(); + $vault = $vaultfactory->get_forum_vault(); + + return $vault->get_from_id((int) $forum->id); + } + + /** + * Get test data for forums graded using a rubric. + * + * @return array + */ + protected function get_test_data(): array { + global $DB; + + $this->resetAfterTest(); + + $generator = \testing_util::get_data_generator(); + $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric'); + + $forum = $this->get_forum_instance(); + $course = $forum->get_course_record(); + $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); + $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + $this->setUser($teacher); + $controller = $rubricgenerator->get_test_rubric($forum->get_context(), 'forum', 'forum'); + $definition = $controller->get_definition(); + + $DB->set_field('forum', 'grade_forum', count($definition->rubric_criteria), ['id' => $forum->get_id()]); + return [ + 'forum' => $forum, + 'controller' => $controller, + 'definition' => $definition, + 'student' => $student, + 'teacher' => $teacher, + ]; + } +} diff --git a/grade/grading/form/rubric/version.php b/grade/grading/form/rubric/version.php index 0b35f3dfc3f..f08c267090a 100644 --- a/grade/grading/form/rubric/version.php +++ b/grade/grading/form/rubric/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'gradingform_rubric'; -$plugin->version = 2019052000; +$plugin->version = 2019052006; $plugin->requires = 2019051100; diff --git a/mod/forum/amd/build/grades/grader.min.js.map b/mod/forum/amd/build/grades/grader.min.js.map index 2769244a14f..aff647e73d1 100644 --- a/mod/forum/amd/build/grades/grader.min.js.map +++ b/mod/forum/amd/build/grades/grader.min.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../src/grades/grader.js"],"names":["templateNames","contentRegion","getWholeForumFunctions","cmid","getPostContextFunction","userid","Repository","getDiscussionByUserID","getContentForUserId","getContentForUserIdFunction","postContextFunction","then","context","discussions","map","discussionPostMapper","Templates","render","catch","Notification","exception","getUsers","getUsersForCmidFunction","CourseRepository","getUsersFromCourseModuleID","users","findGradableNode","node","closest","Selectors","gradableItem","discussion","parentMap","Map","posts","parentposts","forEach","post","set","id","userPosts","userposts","subject","readonly","starter","parentid","parent","get","name","launchWholeForumGrading","rootNode","data","dataset","wholeForumFunctions","Grader","getGradingPanelFunctions","contextid","gradingComponent","gradingComponentSubtype","gradableItemtype","gradingPanelFunctions","launch","getter","setter","groupid","initialUserId","initialuserid","moduleName","registerLaunchListeners","document","addEventListener","e","target","matches","Error","gradableItems","wholeForum","preventDefault"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/grades/grader\n * @package mod_forum\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as Selectors from './grader/selectors';\nimport Repository from 'mod_forum/repository';\nimport Templates from 'core/templates';\nimport * as Grader from '../local/grades/grader';\nimport Notification from 'core/notification';\nimport CourseRepository from 'core_course/repository';\n\nconst templateNames = {\n contentRegion: 'mod_forum/grades/grader/discussion/posts',\n};\n\nconst getWholeForumFunctions = (cmid) => {\n const getPostContextFunction = () => {\n return (userid) => {\n return Repository.getDiscussionByUserID(userid, cmid);\n };\n };\n\n const getContentForUserIdFunction = () => {\n const postContextFunction = getPostContextFunction(cmid);\n return userid => {\n return postContextFunction(userid)\n .then(context => {\n // Rebuild the returned data for the template.\n context.discussions = context.discussions.map(discussionPostMapper);\n\n return Templates.render(templateNames.contentRegion, context);\n })\n .catch(Notification.exception);\n };\n };\n\n const getUsersForCmidFunction = () => {\n return () => {\n return CourseRepository.getUsersFromCourseModuleID(cmid)\n .then((context) => {\n return context.users;\n })\n .catch(Notification.exception);\n };\n };\n\n return {\n getContentForUserId: getContentForUserIdFunction(),\n getUsers: getUsersForCmidFunction()\n };\n};\n\nconst findGradableNode = (node) => {\n return node.closest(Selectors.gradableItem);\n};\n\nconst discussionPostMapper = discussion => {\n // Map postid => post.\n const parentMap = new Map();\n discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post));\n\n const userPosts = discussion.posts.userposts.map(post => {\n post.subject = null;\n post.readonly = true;\n post.starter = !post.parentid;\n post.parent = parentMap.get(post.parentid);\n\n return post;\n });\n\n return {\n id: discussion.id,\n name: discussion.name,\n posts: userPosts,\n };\n};\n\n/**\n * Launch the Grader.\n *\n * @param {HTMLElement} rootNode the root HTML element describing what is to be graded\n */\nconst launchWholeForumGrading = async rootNode => {\n const data = rootNode.dataset;\n const wholeForumFunctions = getWholeForumFunctions(data.cmid);\n const gradingPanelFunctions = await Grader.getGradingPanelFunctions(\n 'mod_forum',\n data.contextid,\n data.gradingComponent,\n data.gradingComponentSubtype,\n data.gradableItemtype\n );\n\n await Grader.launch(\n wholeForumFunctions.getUsers,\n wholeForumFunctions.getContentForUserId,\n gradingPanelFunctions.getter,\n gradingPanelFunctions.setter,\n {\n groupid: data.groupid,\n initialUserId: data.initialuserid,\n moduleName: data.name\n }\n );\n};\n\n/**\n * Register listeners to launch the grading panel.\n */\nexport const registerLaunchListeners = () => {\n document.addEventListener('click', async e => {\n if (e.target.matches(Selectors.launch)) {\n const rootNode = findGradableNode(e.target);\n\n if (!rootNode) {\n throw Error('Unable to find a gradable item');\n }\n\n if (rootNode.matches(Selectors.gradableItems.wholeForum)) {\n // Note: The preventDefault must be before any async function calls because the function becomes async\n // at that point and the default action is implemented.\n e.preventDefault();\n try {\n await launchWholeForumGrading(rootNode);\n } catch (error) {\n Notification.exception(error);\n }\n } else {\n throw Error('Unable to find a valid gradable item');\n }\n }\n });\n};\n"],"file":"grader.min.js"} \ No newline at end of file +{"version":3,"sources":["../../src/grades/grader.js"],"names":["templateNames","contentRegion","getWholeForumFunctions","cmid","getPostContextFunction","userid","Repository","getDiscussionByUserID","getContentForUserId","getContentForUserIdFunction","postContextFunction","then","context","discussions","map","discussionPostMapper","Templates","render","catch","Notification","exception","getUsers","getUsersForCmidFunction","CourseRepository","getUsersFromCourseModuleID","users","findGradableNode","node","closest","Selectors","gradableItem","discussion","parentMap","Map","posts","parentposts","forEach","post","set","id","userPosts","userposts","subject","readonly","starter","parentid","parent","get","name","launchWholeForumGrading","rootNode","data","dataset","wholeForumFunctions","Grader","getGradingPanelFunctions","contextid","gradingComponent","gradingComponentSubtype","gradableItemtype","gradingPanelFunctions","launch","getter","setter","groupid","initialUserId","initialuserid","moduleName","registerLaunchListeners","document","addEventListener","e","target","matches","Error","gradableItems","wholeForum","preventDefault"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * This module will tie together all of the different calls the gradable module will make.\n *\n * @module mod_forum/grades/grader\n * @package mod_forum\n * @copyright 2019 Andrew Nicols \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport * as Selectors from './grader/selectors';\nimport Repository from 'mod_forum/repository';\nimport Templates from 'core/templates';\nimport * as Grader from '../local/grades/grader';\nimport Notification from 'core/notification';\nimport CourseRepository from 'core_course/repository';\n\nconst templateNames = {\n contentRegion: 'mod_forum/grades/grader/discussion/posts',\n};\n\nconst getWholeForumFunctions = (cmid) => {\n const getPostContextFunction = () => {\n return (userid) => {\n return Repository.getDiscussionByUserID(userid, cmid);\n };\n };\n\n const getContentForUserIdFunction = () => {\n const postContextFunction = getPostContextFunction(cmid);\n return userid => {\n return postContextFunction(userid)\n .then(context => {\n // Rebuild the returned data for the template.\n context.discussions = context.discussions.map(discussionPostMapper);\n\n return Templates.render(templateNames.contentRegion, context);\n })\n .catch(Notification.exception);\n };\n };\n\n const getUsersForCmidFunction = () => {\n return () => {\n return CourseRepository.getUsersFromCourseModuleID(cmid)\n .then((context) => {\n return context.users;\n })\n .catch(Notification.exception);\n };\n };\n\n return {\n getContentForUserId: getContentForUserIdFunction(),\n getUsers: getUsersForCmidFunction(),\n };\n};\n\nconst findGradableNode = (node) => {\n return node.closest(Selectors.gradableItem);\n};\n\nconst discussionPostMapper = discussion => {\n // Map postid => post.\n const parentMap = new Map();\n discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post));\n const userPosts = discussion.posts.userposts.map(post => {\n post.subject = null;\n post.readonly = true;\n post.starter = !post.parentid;\n post.parent = parentMap.get(post.parentid);\n\n return post;\n });\n\n return {\n id: discussion.id,\n name: discussion.name,\n posts: userPosts,\n };\n};\n\n/**\n * Launch the Grader.\n *\n * @param {HTMLElement} rootNode the root HTML element describing what is to be graded\n */\nconst launchWholeForumGrading = async rootNode => {\n const data = rootNode.dataset;\n const wholeForumFunctions = getWholeForumFunctions(data.cmid);\n const gradingPanelFunctions = await Grader.getGradingPanelFunctions(\n 'mod_forum',\n data.contextid,\n data.gradingComponent,\n data.gradingComponentSubtype,\n data.gradableItemtype\n );\n\n await Grader.launch(\n wholeForumFunctions.getUsers,\n wholeForumFunctions.getContentForUserId,\n gradingPanelFunctions.getter,\n gradingPanelFunctions.setter,\n {\n groupid: data.groupid,\n initialUserId: data.initialuserid,\n moduleName: data.name\n }\n );\n};\n\n/**\n * Register listeners to launch the grading panel.\n */\nexport const registerLaunchListeners = () => {\n document.addEventListener('click', async e => {\n if (e.target.matches(Selectors.launch)) {\n const rootNode = findGradableNode(e.target);\n\n if (!rootNode) {\n throw Error('Unable to find a gradable item');\n }\n\n if (rootNode.matches(Selectors.gradableItems.wholeForum)) {\n // Note: The preventDefault must be before any async function calls because the function becomes async\n // at that point and the default action is implemented.\n e.preventDefault();\n try {\n await launchWholeForumGrading(rootNode);\n } catch (error) {\n Notification.exception(error);\n }\n } else {\n throw Error('Unable to find a valid gradable item');\n }\n }\n });\n};\n"],"file":"grader.min.js"} \ No newline at end of file diff --git a/mod/forum/amd/src/grades/grader.js b/mod/forum/amd/src/grades/grader.js index 2ff4512d594..6a6f49cc99c 100644 --- a/mod/forum/amd/src/grades/grader.js +++ b/mod/forum/amd/src/grades/grader.js @@ -65,7 +65,7 @@ const getWholeForumFunctions = (cmid) => { return { getContentForUserId: getContentForUserIdFunction(), - getUsers: getUsersForCmidFunction() + getUsers: getUsersForCmidFunction(), }; }; @@ -77,7 +77,6 @@ const discussionPostMapper = discussion => { // Map postid => post. const parentMap = new Map(); discussion.posts.parentposts.forEach(post => parentMap.set(post.id, post)); - const userPosts = discussion.posts.userposts.map(post => { post.subject = null; post.readonly = true; diff --git a/mod/forum/templates/local/grades/local/grader/grading.mustache b/mod/forum/templates/local/grades/local/grader/grading.mustache index 997b4c3cf43..fab4025f714 100644 --- a/mod/forum/templates/local/grades/local/grader/grading.mustache +++ b/mod/forum/templates/local/grades/local/grader/grading.mustache @@ -42,8 +42,8 @@
-

-

Grade:

+

+

{{#str}}grade, core_grades{{/str}}

{{> mod_forum/local/grades/local/grader/grade_placeholder }}
diff --git a/theme/boost/scss/moodle/grade.scss b/theme/boost/scss/moodle/grade.scss index ebf5bc43304..8f5b35f35d2 100644 --- a/theme/boost/scss/moodle/grade.scss +++ b/theme/boost/scss/moodle/grade.scss @@ -240,6 +240,22 @@ } } +.criterion button.collapse[aria-expanded="true"]:before { + content: $fa-var-angle-down; + margin-right: 0; + @include fa-icon(); + font-size: 16px; + width: 16px; +} + +.criterion button.collapse[aria-expanded="false"]:before { + content: $fa-var-angle-up; + margin-right: 0; + @include fa-icon(); + font-size: 16px; + width: 16px; +} + // Set up grades layout. .path-grade-edit-tree .setup-grades { h4 { diff --git a/theme/boost/style/moodle.css b/theme/boost/style/moodle.css index 743908956ec..e07cc6c1b77 100644 --- a/theme/boost/style/moodle.css +++ b/theme/boost/style/moodle.css @@ -17367,6 +17367,30 @@ p.arrow_button { margin-left: 5px; margin-right: 12px; } +.criterion button.collapse[aria-expanded="true"]:before { + content: ""; + margin-right: 0; + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-size: 16px; + width: 16px; } + +.criterion button.collapse[aria-expanded="false"]:before { + content: ""; + margin-right: 0; + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-size: 16px; + width: 16px; } + .path-grade-edit-tree .setup-grades h4 { margin: 0; } diff --git a/theme/classic/style/moodle.css b/theme/classic/style/moodle.css index 134f5896052..8870dd8d3cb 100644 --- a/theme/classic/style/moodle.css +++ b/theme/classic/style/moodle.css @@ -17643,6 +17643,30 @@ p.arrow_button { margin-left: 5px; margin-right: 12px; } +.criterion button.collapse[aria-expanded="true"]:before { + content: ""; + margin-right: 0; + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-size: 16px; + width: 16px; } + +.criterion button.collapse[aria-expanded="false"]:before { + content: ""; + margin-right: 0; + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-size: 16px; + width: 16px; } + .path-grade-edit-tree .setup-grades h4 { margin: 0; }