first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,56 @@
<?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/>.
/**
* This file contains the polyfill to allow a plugin to operate with Moodle 3.3 up.
*
* @package core_grading
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_grading\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* The trait used to provide backwards compatability for third-party plugins.
*
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
trait gradingform_legacy_polyfill {
/**
* Export user data relating to an instance ID.
*
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
public static function export_gradingform_instance_data(\context $context, int $instanceid, array $subcontext) {
static::_export_gradingform_instance_data($context, $instanceid, $subcontext);
}
/**
* Deletes all user data related to the provided instance IDs.
*
* @param array $instanceids The instance IDs to delete information from.
*/
public static function delete_gradingform_for_instances(array $instanceids) {
static::_delete_gradingform_for_instances($instanceids);
}
}
@@ -0,0 +1,48 @@
<?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/>.
/**
* Privacy class for requesting user data.
*
* @package core_grading
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_grading\privacy;
defined('MOODLE_INTERNAL') || die();
interface gradingform_provider_v2 extends
\core_privacy\local\request\plugin\subsystem_provider,
\core_privacy\local\request\shared_userlist_provider
{
/**
* Export user data relating to an instance ID.
*
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
public static function export_gradingform_instance_data(\context $context, int $instanceid, array $subcontext);
/**
* Deletes all user data related to the provided instance IDs.
*
* @param array $instanceids The instance IDs to delete information from.
*/
public static function delete_gradingform_for_instances(array $instanceids);
}
+389
View File
@@ -0,0 +1,389 @@
<?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/>.
/**
* Privacy class for requesting user data.
*
* @package core_grading
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_grading\privacy;
defined('MOODLE_INTERNAL') || die();
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\approved_contextlist;
use \core_privacy\local\request\contextlist;
use \core_privacy\local\request\transform;
use \core_privacy\local\request\writer;
use \core_privacy\manager;
/**
* Privacy class for requesting user data.
*
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\plugin\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\subsystem\provider {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table('grading_definitions', [
'method' => 'privacy:metadata:grading_definitions:method',
'areaid' => 'privacy:metadata:grading_definitions:areaid',
'name' => 'privacy:metadata:grading_definitions:name',
'description' => 'privacy:metadata:grading_definitions:description',
'status' => 'privacy:metadata:grading_definitions:status',
'copiedfromid' => 'privacy:metadata:grading_definitions:copiedfromid',
'timecopied' => 'privacy:metadata:grading_definitions:timecopied',
'timecreated' => 'privacy:metadata:grading_definitions:timecreated',
'usercreated' => 'privacy:metadata:grading_definitions:usercreated',
'timemodified' => 'privacy:metadata:grading_definitions:timemodified',
'usermodified' => 'privacy:metadata:grading_definitions:usermodified',
'options' => 'privacy:metadata:grading_definitions:options',
], 'privacy:metadata:grading_definitions');
$collection->add_database_table('grading_instances', [
'raterid' => 'privacy:metadata:grading_instances:raterid',
'rawgrade' => 'privacy:metadata:grading_instances:rawgrade',
'status' => 'privacy:metadata:grading_instances:status',
'feedback' => 'privacy:metadata:grading_instances:feedback',
'feedbackformat' => 'privacy:metadata:grading_instances:feedbackformat',
'timemodified' => 'privacy:metadata:grading_instances:timemodified',
], 'privacy:metadata:grading_instances');
// Link to subplugin.
$collection->add_plugintype_link('gradingform', [], 'privacy:metadata:gradingformpluginsummary');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$contextlist = new contextlist();
$sql = "SELECT c.id
FROM {context} c
JOIN {grading_areas} a ON a.contextid = c.id
JOIN {grading_definitions} d ON d.areaid = a.id
LEFT JOIN {grading_instances} i ON i.definitionid = d.id AND i.raterid = :raterid
WHERE c.contextlevel = :contextlevel
AND (d.usercreated = :usercreated OR d.usermodified = :usermodified OR i.id IS NOT NULL)";
$params = [
'usercreated' => $userid,
'usermodified' => $userid,
'raterid' => $userid,
'contextlevel' => CONTEXT_MODULE
];
$contextlist->add_from_sql($sql, $params);
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(\core_privacy\local\request\userlist $userlist) {
$context = $userlist->get_context();
if ($context->contextlevel != CONTEXT_MODULE) {
return;
}
$params = ['contextid' => $context->id];
$sql = "SELECT d.usercreated, d.usermodified
FROM {grading_definitions} d
JOIN {grading_areas} a ON a.id = d.areaid
WHERE a.contextid = :contextid";
$userlist->add_from_sql('usercreated', $sql, $params);
$userlist->add_from_sql('usermodified', $sql, $params);
$sql = "SELECT i.raterid
FROM {grading_definitions} d
JOIN {grading_areas} a ON a.id = d.areaid
JOIN {grading_instances} i ON i.definitionid = d.id
WHERE a.contextid = :contextid";
$userlist->add_from_sql('raterid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
// Remove contexts different from MODULE.
$contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
if ($context->contextlevel == CONTEXT_MODULE) {
$carry[] = $context;
}
return $carry;
}, []);
if (empty($contexts)) {
return;
}
$userid = $contextlist->get_user()->id;
$subcontext = [get_string('gradingmethod', 'grading')];
foreach ($contexts as $context) {
// Export grading definitions created or modified on this context.
self::export_definitions($context, $subcontext, $userid);
}
}
/**
* Export all user data related to a context and itemid.
*
* @param \context $context Context to export on.
* @param int $itemid Item ID to export on.
* @param array $subcontext Directory location to export to.
*/
public static function export_item_data(\context $context, int $itemid, array $subcontext) {
global $DB;
$sql = "SELECT gi.id AS instanceid, gd.id AS definitionid, gd.method
FROM {grading_areas} ga
JOIN {grading_definitions} gd ON gd.areaid = ga.id
JOIN {grading_instances} gi ON gi.definitionid = gd.id AND gi.itemid = :itemid
WHERE ga.contextid = :contextid";
$params = [
'itemid' => $itemid,
'contextid' => $context->id,
];
$records = $DB->get_recordset_sql($sql, $params);
foreach ($records as $record) {
$instancedata = manager::component_class_callback(
"gradingform_{$record->method}",
gradingform_provider_v2::class,
'export_gradingform_instance_data',
[$context, $record->instanceid, $subcontext]
);
}
$records->close();
}
/**
* Deletes all user data related to a context and possibly an itemid.
*
* @param \context $context The context to delete on.
* @param int|null $itemid An optional item ID to refine the deletion.
*/
public static function delete_instance_data(\context $context, int $itemid = null) {
if (is_null($itemid)) {
self::delete_data_for_instances($context);
} else {
self::delete_data_for_instances($context, [$itemid]);
}
}
/**
* Deletes all user data related to a context and possibly itemids.
*
* @param \context $context The context to delete on.
* @param array $itemids An optional list of item IDs to refine the deletion.
*/
public static function delete_data_for_instances(\context $context, array $itemids = []) {
global $DB;
$itemsql = '';
$params = ['contextid' => $context->id];
if (!empty($itemids)) {
list($itemsql, $itemparams) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED);
$params = array_merge($params, $itemparams);
$itemsql = "AND itemid $itemsql";
}
$sql = "SELECT gi.id AS instanceid, gd.id, gd.method
FROM {grading_definitions} gd
JOIN {grading_instances} gi ON gi.definitionid = gd.id
JOIN {grading_areas} ga ON ga.id = gd.areaid
WHERE ga.contextid = :contextid $itemsql";
$records = $DB->get_records_sql($sql, $params);
if ($records) {
$firstrecord = current($records);
$method = $firstrecord->method;
$instanceids = array_map(function($record) {
return $record->instanceid;
}, $records);
manager::component_class_callback(
"gradingform_{$method}",
gradingform_provider_v2::class,
'delete_gradingform_for_instances',
[$instanceids]);
// Delete grading_instances rows.
$DB->delete_records_list('grading_instances', 'id', $instanceids);
}
}
/**
* Exports the data related to grading definitions within the specified context/subcontext.
*
* @param \context $context Context owner of the data.
* @param array $subcontext Subcontext owner of the data.
* @param int $userid The user whose information is to be exported.
*/
protected static function export_definitions(\context $context, array $subcontext, int $userid = 0) {
global $DB;
$join = "JOIN {grading_areas} a ON a.id = d.areaid
JOIN {context} c ON a.contextid = c.id AND c.contextlevel = :contextlevel";
$select = 'a.contextid = :contextid';
$params = [
'contextlevel' => CONTEXT_MODULE,
'contextid' => $context->id
];
if (!empty($userid)) {
$join .= ' LEFT JOIN {grading_instances} i ON i.definitionid = d.id AND i.raterid = :raterid';
$select .= ' AND (usercreated = :usercreated
OR usermodified = :usermodified OR i.id IS NOT NULL)';
$params['usercreated'] = $userid;
$params['usermodified'] = $userid;
$params['raterid'] = $userid;
}
$sql = "SELECT gd.id,
gd.method,
gd.name,
gd.description,
gd.timecopied,
gd.timecreated,
gd.usercreated,
gd.timemodified,
gd.usermodified
FROM (
SELECT DISTINCT d.id
FROM {grading_definitions} d
$join
WHERE $select
) ids
JOIN {grading_definitions} gd ON gd.id = ids.id";
$definitions = $DB->get_recordset_sql($sql, $params);
$defdata = [];
foreach ($definitions as $definition) {
$tmpdata = [
'method' => $definition->method,
'name' => $definition->name,
'description' => $definition->description,
'timecreated' => transform::datetime($definition->timecreated),
'usercreated' => transform::user($definition->usercreated),
'timemodified' => transform::datetime($definition->timemodified),
'usermodified' => transform::user($definition->usermodified),
];
if (!empty($definition->timecopied)) {
$tmpdata['timecopied'] = transform::datetime($definition->timecopied);
}
$defdata[] = (object) $tmpdata;
// Export grading_instances information.
self::export_grading_instances($context, $subcontext, $definition->id, $userid);
}
$definitions->close();
if (!empty($defdata)) {
$data = (object) [
'definitions' => $defdata,
];
writer::with_context($context)->export_data($subcontext, $data);
}
}
/**
* Exports the data related to grading instances within the specified definition.
*
* @param \context $context Context owner of the data.
* @param array $subcontext Subcontext owner of the data.
* @param int $definitionid The definition ID whose grading instance information is to be exported.
* @param int $userid The user whose information is to be exported.
*/
protected static function export_grading_instances(\context $context, array $subcontext, int $definitionid, int $userid = 0) {
global $DB;
$params = ['definitionid' => $definitionid];
if (!empty($userid)) {
$params['raterid'] = $userid;
}
$instances = $DB->get_recordset('grading_instances', $params);
$instancedata = [];
foreach ($instances as $instance) {
// TODO: Get the status name (instead of the ID).
$tmpdata = [
'rawgrade' => $instance->rawgrade,
'status' => $instance->status,
'feedback' => $instance->feedback,
'feedbackformat' => $instance->feedbackformat,
'timemodified' => transform::datetime($instance->timemodified),
];
$instancedata[] = (object) $tmpdata;
}
$instances->close();
if (!empty($instancedata)) {
$data = (object) [
'instances' => $instancedata,
];
writer::with_context($context)->export_related_data($subcontext, 'gradinginstances', $data);
}
}
/**
* No deletion of the advanced grading is done.
*
* @param \context $context the context to delete in.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
// The only information left to be deleted here is the grading definitions. Currently we are not deleting these.
}
/**
* Deletion of data in this provider is only related to grades and so can not be
* deleted for the creator of the advanced grade criteria.
*
* @param approved_contextlist $contextlist a list of contexts approved for deletion.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
// The only information left to be deleted here is the grading definitions. Currently we are not deleting these.
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(\core_privacy\local\request\approved_userlist $userlist) {
// The only information left to be deleted here is the grading definitions. Currently we are not deleting these.
}
}
+4
View File
@@ -0,0 +1,4 @@
Marking Guide grading form written by Dan Marsden <dan@danmarsden.com>
based on Lightwork Rubric type 2 format and the spec available here:
http://docs.moodle.org/dev/Lightwork
@@ -0,0 +1,10 @@
/**
* AMD code for the frequently used comments chooser for the marking guide grading form.
*
* @module gradingform_guide/comment_chooser
* @copyright 2015 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("gradingform_guide/comment_chooser",["jquery","core/templates","core/key_codes","core/notification","core/yui"],(function($,templates,keycodes,notification){return{initialise:function(criterionId,buttonId,remarkId,commentOptions){function generateCommentsChooser(){var context={criterionId:criterionId,comments:commentOptions};templates.render("gradingform_guide/comment_chooser",context).done((function(compiledSource){!function(compiledSource,comments){var titleLabel="<label>"+M.util.get_string("insertcomment","gradingform_guide")+"</label>",cancelButtonId="comment-chooser-"+criterionId+"-cancel",cancelButton='<button id="'+cancelButtonId+'">'+M.util.get_string("cancel","moodle")+"</button>",chooserDialog=new M.core.dialogue({modal:!0,headerContent:titleLabel,bodyContent:compiledSource,footerContent:cancelButton,focusAfterHide:"#"+remarkId,id:"comments-chooser-dialog-"+criterionId});$("#"+cancelButtonId).click((function(){chooserDialog.hide()})),$.each(comments,(function(index,comment){var commentOptionId="#comment-option-"+criterionId+"-"+comment.id;$(commentOptionId).click((function(){var remarkTextArea=$("#"+remarkId),remarkText=remarkTextArea.val();""!==remarkText.trim()&&(remarkText+="\n"),remarkText+=comment.description,remarkTextArea.val(remarkText),chooserDialog.hide()})),$(document).off("keypress",commentOptionId).on("keypress",commentOptionId,(function(){(event.which||event.keyCode)===keycodes.space&&$(commentOptionId).click()}))})),chooserDialog.after("visibleChange",(function(e){e.prevVal&&!e.newVal&&this.destroy()}),chooserDialog),chooserDialog.show()}(compiledSource,commentOptions)})).fail(notification.exception)}$("#"+buttonId).click((function(e){e.preventDefault(),generateCommentsChooser()}))}}}));
//# sourceMappingURL=comment_chooser.min.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
define("gradingform_guide/grades/grader/gradingpanel",["exports","core/ajax","core_grades/grades/grader/gradingpanel/normalise","core_grades/grades/grader/gradingpanel/comparison","jquery"],(function(_exports,_ajax,_normalise,_comparison,_jquery){var obj;
/**
* Grading panel for gradingform_guide.
*
* @module gradingform_guide/grades/grader/gradingpanel
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.storeCurrentGrade=_exports.fetchCurrentGrade=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.fetchCurrentGrade=(component,contextid,itemname,gradeduserid)=>(0,_ajax.call)([{methodname:"gradingform_guide_grader_gradingpanel_fetch",args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid}}])[0];_exports.storeCurrentGrade=async(component,contextid,itemname,gradeduserid,notifyUser,rootNode)=>{const form=rootNode.querySelector("form");return!0===(0,_comparison.compareData)(form)?(0,_normalise.normaliseResult)(await(0,_ajax.call)([{methodname:"gradingform_guide_grader_gradingpanel_store",args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid,notifyuser:notifyUser,formdata:(0,_jquery.default)(form).serialize()}}])[0]):""}}));
//# sourceMappingURL=gradingpanel.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"gradingpanel.min.js","sources":["../../../src/grades/grader/gradingpanel.js"],"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Grading panel for gradingform_guide.\n *\n * @module gradingform_guide/grades/grader/gradingpanel\n * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>\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';\nimport {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';\n\n// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()\nimport jQuery from 'jquery';\n\n/**\n * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade.\n *\n * @param {String} component\n * @param {Number} contextid\n * @param {String} itemname\n * @param {Number} gradeduserid\n *\n * @returns {Promise}\n */\nexport const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {\n return fetchMany([{\n methodname: `gradingform_guide_grader_gradingpanel_fetch`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n },\n }])[0];\n};\n\n/**\n * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form.\n *\n * @param {String} component\n * @param {Number} contextid\n * @param {String} itemname\n * @param {Number} gradeduserid\n * @param {Boolean} notifyUser\n * @param {HTMLElement} rootNode\n *\n * @returns {Promise}\n */\nexport const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, notifyUser, rootNode) => {\n const form = rootNode.querySelector('form');\n\n if (compareData(form) === true) {\n return normaliseResult(await fetchMany([{\n methodname: `gradingform_guide_grader_gradingpanel_store`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n notifyuser: notifyUser,\n formdata: jQuery(form).serialize(),\n },\n }])[0]);\n } else {\n return '';\n }\n};\n"],"names":["component","contextid","itemname","gradeduserid","methodname","args","async","notifyUser","rootNode","form","querySelector","notifyuser","formdata","serialize"],"mappings":";;;;;;;6MAwCiC,CAACA,UAAWC,UAAWC,SAAUC,gBACvD,cAAU,CAAC,CACdC,yDACAC,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,iBAEJ,8BAeyBG,MAAMN,UAAWC,UAAWC,SAAUC,aAAcI,WAAYC,kBACvFC,KAAOD,SAASE,cAAc,eAEV,KAAtB,2BAAYD,OACL,oCAAsB,cAAU,CAAC,CACpCL,yDACAC,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,aACAQ,WAAYJ,WACZK,UAAU,mBAAOH,MAAMI,gBAE3B,IAEG"}
@@ -0,0 +1,10 @@
define("gradingform_guide/grades/grader/gradingpanel/comments",["exports","./comments/selectors"],(function(_exports,_selectors){var obj;
/**
* Grading panel frequently used comments selector.
*
* @module gradingform_guide/grades/grader/gradingpanel/comments
* @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_selectors=(obj=_selectors)&&obj.__esModule?obj:{default:obj};_exports.init=rootId=>{document.querySelector("#".concat(rootId)).addEventListener("click",(e=>{if(!e.target.matches(_selectors.default.frequentComment))return;e.preventDefault();const clicked=e.target.closest(_selectors.default.frequentComment),remark=clicked.closest(_selectors.default.criterion).querySelector(_selectors.default.remark);remark&&(remark.value.trim()?remark.value+="\n".concat(clicked.innerHTML):remark.value+=clicked.innerHTML)}))}}));
//# sourceMappingURL=comments.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"comments.min.js","sources":["../../../../src/grades/grader/gradingpanel/comments.js"],"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Grading panel frequently used comments selector.\n *\n * @module gradingform_guide/grades/grader/gradingpanel/comments\n * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Selectors from './comments/selectors';\n\n/**\n * Manage the frequently used comments in the Marking Guide form.\n *\n * @param {String} rootId\n */\nexport const init = (rootId) => {\n const rootNode = document.querySelector(`#${rootId}`);\n\n rootNode.addEventListener('click', (e) => {\n if (!e.target.matches(Selectors.frequentComment)) {\n return;\n }\n\n e.preventDefault();\n\n const clicked = e.target.closest(Selectors.frequentComment);\n const criterion = clicked.closest(Selectors.criterion);\n const remark = criterion.querySelector(Selectors.remark);\n\n if (!remark) {\n return;\n }\n\n // Either append the comment to an existing comment or set it as the comment.\n if (remark.value.trim()) {\n remark.value += `\\n${clicked.innerHTML}`;\n } else {\n remark.value += clicked.innerHTML;\n }\n });\n};\n"],"names":["rootId","document","querySelector","addEventListener","e","target","matches","Selectors","frequentComment","preventDefault","clicked","closest","remark","criterion","value","trim","innerHTML"],"mappings":";;;;;;;8JA8BqBA,SACAC,SAASC,yBAAkBF,SAEnCG,iBAAiB,SAAUC,QAC3BA,EAAEC,OAAOC,QAAQC,mBAAUC,wBAIhCJ,EAAEK,uBAEIC,QAAUN,EAAEC,OAAOM,QAAQJ,mBAAUC,iBAErCI,OADYF,QAAQC,QAAQJ,mBAAUM,WACnBX,cAAcK,mBAAUK,QAE5CA,SAKDA,OAAOE,MAAMC,OACbH,OAAOE,mBAAcJ,QAAQM,WAE7BJ,OAAOE,OAASJ,QAAQM"}
@@ -0,0 +1,3 @@
define("gradingform_guide/grades/grader/gradingpanel/comments/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={frequentComment:'[data-gradingform_guide-role="frequent-comment"]',criterion:'[data-gradingform-guide-role="criterion"]',remark:'[data-gradingform-guide-role="remark"]'},_exports.default}));
//# sourceMappingURL=selectors.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"selectors.min.js","sources":["../../../../../src/grades/grader/gradingpanel/comments/selectors.js"],"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Define all of the selectors we will be using on the Marking Guide interface.\n *\n * @module gradingform_guide/grades/grader/gradingpanel/comments/selectors\n * @copyright 2019 Mathew May <mathew.solutions>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nexport default {\n frequentComment: '[data-gradingform_guide-role=\"frequent-comment\"]',\n criterion: '[data-gradingform-guide-role=\"criterion\"]',\n remark: '[data-gradingform-guide-role=\"remark\"]',\n};\n"],"names":["frequentComment","criterion","remark"],"mappings":"iNAsBe,CACXA,gBAAiB,mDACjBC,UAAW,4CACXC,OAAQ"}
@@ -0,0 +1,138 @@
// 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/>.
/**
* AMD code for the frequently used comments chooser for the marking guide grading form.
*
* @module gradingform_guide/comment_chooser
* @copyright 2015 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/templates', 'core/key_codes', 'core/notification', 'core/yui'],
function($, templates, keycodes, notification) {
// Private variables and functions.
return /** @alias module:gradingform_guide/comment_chooser */ {
// Public variables and functions.
/**
* Initialises the module.
*
* Basically, it performs the binding and handling of the button click event for
* the 'Insert frequently used comment' button.
*
* @param {Integer} criterionId The criterion ID.
* @param {String} buttonId The element ID of the button which the handler will be bound to.
* @param {String} remarkId The element ID of the remark text area where the text of the selected comment will be copied to.
* @param {Array} commentOptions The array of frequently used comments to be used as options.
*/
initialise: function(criterionId, buttonId, remarkId, commentOptions) {
/**
* Display the chooser dialog using the compiled HTML from the mustache template
* and binds onclick events for the generated comment options.
*
* @param {String} compiledSource The compiled HTML from the mustache template
* @param {Array} comments Array containing comments.
*/
function displayChooserDialog(compiledSource, comments) {
var titleLabel = '<label>' + M.util.get_string('insertcomment', 'gradingform_guide') + '</label>';
var cancelButtonId = 'comment-chooser-' + criterionId + '-cancel';
var cancelButton = '<button id="' + cancelButtonId + '">' + M.util.get_string('cancel', 'moodle') + '</button>';
// Set dialog's body content.
var chooserDialog = new M.core.dialogue({
modal: true,
headerContent: titleLabel,
bodyContent: compiledSource,
footerContent: cancelButton,
focusAfterHide: '#' + remarkId,
id: "comments-chooser-dialog-" + criterionId
});
// Bind click event to the cancel button.
$("#" + cancelButtonId).click(function() {
chooserDialog.hide();
});
// Loop over each comment item and bind click events.
$.each(comments, function(index, comment) {
var commentOptionId = '#comment-option-' + criterionId + '-' + comment.id;
// Delegate click event for the generated option link.
$(commentOptionId).click(function() {
var remarkTextArea = $('#' + remarkId);
var remarkText = remarkTextArea.val();
// Add line break if the current value of the remark text is not empty.
if (remarkText.trim() !== '') {
remarkText += '\n';
}
remarkText += comment.description;
remarkTextArea.val(remarkText);
chooserDialog.hide();
});
// Handle keypress on list items.
$(document).off('keypress', commentOptionId).on('keypress', commentOptionId, function() {
var keyCode = event.which || event.keyCode;
// Trigger click event when user presses space.
if (keyCode === keycodes.space) {
$(commentOptionId).click();
}
});
});
// Destroy the dialog when it is hidden to allow the grading section to
// be loaded as a fragment multiple times within the same page.
chooserDialog.after('visibleChange', function(e) {
// Going from visible to hidden.
if (e.prevVal && !e.newVal) {
this.destroy();
}
}, chooserDialog);
// Show dialog.
chooserDialog.show();
}
/**
* Generates the comments chooser dialog from the grading_form/comment_chooser mustache template.
*/
function generateCommentsChooser() {
// Template context.
var context = {
criterionId: criterionId,
comments: commentOptions
};
// Render the template and display the comment chooser dialog.
templates.render('gradingform_guide/comment_chooser', context)
.done(function(compiledSource) {
displayChooserDialog(compiledSource, commentOptions);
})
.fail(notification.exception);
}
// Bind click event for the comments chooser button.
$("#" + buttonId).click(function(e) {
e.preventDefault();
generateCommentsChooser();
});
}
};
});
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Grading panel for gradingform_guide.
*
* @module gradingform_guide/grades/grader/gradingpanel
* @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';
import {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';
// 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 {Boolean} notifyUser
* @param {HTMLElement} rootNode
*
* @returns {Promise}
*/
export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, notifyUser, rootNode) => {
const form = rootNode.querySelector('form');
if (compareData(form) === true) {
return normaliseResult(await fetchMany([{
methodname: `gradingform_guide_grader_gradingpanel_store`,
args: {
component,
contextid,
itemname,
gradeduserid,
notifyuser: notifyUser,
formdata: jQuery(form).serialize(),
},
}])[0]);
} else {
return '';
}
};
@@ -0,0 +1,56 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Grading panel frequently used comments selector.
*
* @module gradingform_guide/grades/grader/gradingpanel/comments
* @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;
}
});
};
@@ -0,0 +1,27 @@
// 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
* @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"]',
};
@@ -0,0 +1,122 @@
<?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/>.
/**
* Support for backup API
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Defines marking guide backup structures
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_gradingform_guide_plugin extends backup_gradingform_plugin {
/**
* Declares marking guide structures to append to the grading form definition
* @return backup_plugin_element
*/
protected function define_definition_plugin_structure() {
// Append data only if the grand-parent element has 'method' set to 'guide'.
$plugin = $this->get_plugin_element(null, '../../method', 'guide');
// Create a visible container for our data.
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
// Connect our visible container to the parent.
$plugin->add_child($pluginwrapper);
// Define our elements.
$criteria = new backup_nested_element('guidecriteria');
$criterion = new backup_nested_element('guidecriterion', array('id'), array(
'sortorder', 'shortname', 'description', 'descriptionformat',
'descriptionmarkers', 'descriptionmarkersformat', 'maxscore'));
$comments = new backup_nested_element('guidecomments');
$comment = new backup_nested_element('guidecomment', array('id'), array(
'sortorder', 'description', 'descriptionformat'));
// Build elements hierarchy.
$pluginwrapper->add_child($criteria);
$criteria->add_child($criterion);
$pluginwrapper->add_child($comments);
$comments->add_child($comment);
// Set sources to populate the data.
$criterion->set_source_table('gradingform_guide_criteria',
array('definitionid' => backup::VAR_PARENTID));
$comment->set_source_table('gradingform_guide_comments',
array('definitionid' => backup::VAR_PARENTID));
// No need to annotate ids or files yet (one day when criterion definition supports
// embedded files, they must be annotated here).
return $plugin;
}
/**
* Declares marking guide structures to append to the grading form instances
* @return backup_plugin_element
*/
protected function define_instance_plugin_structure() {
// Append data only if the ancestor 'definition' element has 'method' set to 'guide'.
$plugin = $this->get_plugin_element(null, '../../../../method', 'guide');
// Create a visible container for our data.
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
// Connect our visible container to the parent.
$plugin->add_child($pluginwrapper);
// Define our elements.
$fillings = new backup_nested_element('fillings');
$filling = new backup_nested_element('filling', array('id'), array(
'criterionid', 'remark', 'remarkformat', 'score'));
// Build elements hierarchy.
$pluginwrapper->add_child($fillings);
$fillings->add_child($filling);
// Set sources to populate the data.
$filling->set_source_table('gradingform_guide_fillings',
array('instanceid' => backup::VAR_PARENTID));
// No need to annotate ids or files yet (one day when remark field supports
// embedded fileds, they must be annotated here).
return $plugin;
}
}
@@ -0,0 +1,135 @@
<?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/>.
/**
* Support for restore API
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Restores the marking guide specific data from grading.xml file
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_gradingform_guide_plugin extends restore_gradingform_plugin {
/**
* Declares the marking guide XML paths attached to the form definition element
*
* @return array of {@link restore_path_element}
*/
protected function define_definition_plugin_structure() {
$paths = array();
$paths[] = new restore_path_element('gradingform_guide_criterion',
$this->get_pathfor('/guidecriteria/guidecriterion'));
$paths[] = new restore_path_element('gradingform_guide_comment',
$this->get_pathfor('/guidecomments/guidecomment'));
// MDL-37714: Correctly locate frequent used comments in both the
// current and incorrect old format.
$paths[] = new restore_path_element('gradingform_guide_comment_legacy',
$this->get_pathfor('/guidecriteria/guidecomments/guidecomment'));
return $paths;
}
/**
* Declares the marking guide XML paths attached to the form instance element
*
* @return array of {@link restore_path_element}
*/
protected function define_instance_plugin_structure() {
$paths = array();
$paths[] = new restore_path_element('gradinform_guide_filling',
$this->get_pathfor('/fillings/filling'));
return $paths;
}
/**
* Processes criterion element data
*
* Sets the mapping 'gradingform_guide_criterion' to be used later by
* {@link self::process_gradinform_guide_filling()}
*
* @param array|stdClass $data
*/
public function process_gradingform_guide_criterion($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->definitionid = $this->get_new_parentid('grading_definition');
$newid = $DB->insert_record('gradingform_guide_criteria', $data);
$this->set_mapping('gradingform_guide_criterion', $oldid, $newid);
}
/**
* Processes comments element data
*
* @param array|stdClass $data The data to insert as a comment
*/
public function process_gradingform_guide_comment($data) {
global $DB;
$data = (object)$data;
$data->definitionid = $this->get_new_parentid('grading_definition');
$DB->insert_record('gradingform_guide_comments', $data);
}
/**
* Processes comments element data
*
* @param array|stdClass $data The data to insert as a comment
*/
public function process_gradingform_guide_comment_legacy($data) {
global $DB;
$data = (object)$data;
$data->definitionid = $this->get_new_parentid('grading_definition');
$DB->insert_record('gradingform_guide_comments', $data);
}
/**
* Processes filling element data
*
* @param array|stdClass $data The data to insert as a filling
*/
public function process_gradinform_guide_filling($data) {
global $DB;
$data = (object)$data;
$data->instanceid = $this->get_new_parentid('grading_instance');
$data->criterionid = $this->get_mappingid('gradingform_guide_criterion', $data->criterionid);
$DB->insert_record('gradingform_guide_fillings', $data);
}
}
@@ -0,0 +1,321 @@
<?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 core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\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 $CFG, $USER;
require_once("{$CFG->libdir}/gradelib.php");
[
'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, '*', MUST_EXIST);
// One can access its own grades. Others just if they're graders.
if ($gradeduserid != $USER->id) {
$gradeitem->require_user_can_grade($gradeduser, $USER);
}
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;
$hasgrade = $gradeitem->user_has_grade($gradeduser);
$grade = $gradeitem->get_formatted_grade_for_user($gradeduser, $USER);
$instance = $gradeitem->get_advanced_grading_instance($USER, $grade);
if (!$instance) {
throw new moodle_exception('error:gradingunavailable', 'grading');
}
$controller = $instance->get_controller();
$definition = $controller->get_definition();
$fillings = $instance->get_guide_filling();
$context = $controller->get_context();
$definitionid = (int) $definition->id;
// Set up some items we need to return on other interfaces.
$gradegrade = \grade_grade::fetch(['itemid' => $gradeitem->get_grade_item()->id, 'userid' => $gradeduser->id]);
$gradername = $gradegrade ? fullname(\core_user::get_user($gradegrade->usermodified)) : null;
$maxgrade = max(array_keys($controller->get_grade_range()));
$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',
'hasgrade' => $hasgrade,
'grade' => [
'instanceid' => $instance->get_id(),
'criterion' => $criterion,
'hascomments' => !empty($comments),
'comments' => $comments,
'usergrade' => $grade->usergrade,
'maxgrade' => $maxgrade,
'gradedby' => $gradername,
'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'),
'hasgrade' => new external_value(PARAM_BOOL, 'Does the user have a grade?'),
'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'
),
'usergrade' => new external_value(PARAM_RAW, 'Current user grade'),
'maxgrade' => new external_value(PARAM_RAW, 'Max possible grade'),
'gradedby' => new external_value(PARAM_RAW, 'The assumed grader of this grading instance'),
'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] = \core_external\util::format_text(
$text,
$format,
$context,
'grading',
$filearea,
$definitionid,
$formatoptions
);
return $newtext;
}
}
@@ -0,0 +1,186 @@
<?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 core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\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
),
'notifyuser' => new external_value(
PARAM_BOOL,
'Wheteher to notify the user or not',
VALUE_DEFAULT,
false
),
'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 bool $notifyuser
* @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,
bool $notifyuser, string $formdata): array {
global $USER;
[
'component' => $component,
'contextid' => $contextid,
'itemname' => $itemname,
'gradeduserid' => $gradeduserid,
'notifyuser' => $notifyuser,
'formdata' => $formdata,
] = self::validate_parameters(self::execute_parameters(), [
'component' => $component,
'contextid' => $contextid,
'itemname' => $itemname,
'gradeduserid' => $gradeduserid,
'notifyuser' => $notifyuser,
'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);
// Notify.
if ($notifyuser) {
// Send notification.
$gradeitem->send_student_notification($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();
}
}
@@ -0,0 +1,129 @@
<?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/>.
/**
* Privacy class for requesting user data.
*
* @package gradingform_guide
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace gradingform_guide\privacy;
defined('MOODLE_INTERNAL') || die();
use \core_privacy\local\metadata\collection;
use \core_privacy\local\request\transform;
use \core_privacy\local\request\writer;
/**
* Privacy class for requesting user data.
*
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_grading\privacy\gradingform_provider_v2,
\core_privacy\local\request\user_preference_provider {
/**
* Return the fields which contain personal data.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table('gradingform_guide_fillings', [
'instanceid' => 'privacy:metadata:instanceid',
'criterionid' => 'privacy:metadata:criterionid',
'remark' => 'privacy:metadata:remark',
'score' => 'privacy:metadata:score'
], 'privacy:metadata:fillingssummary');
$collection->add_user_preference(
'gradingform_guide-showmarkerdesc',
'privacy:metadata:preference:showmarkerdesc'
);
$collection->add_user_preference(
'gradingform_guide-showstudentdesc',
'privacy:metadata:preference:showstudentdesc'
);
return $collection;
}
/**
* Export user data relating to an instance ID.
*
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
public static function export_gradingform_instance_data(\context $context, int $instanceid, array $subcontext) {
global $DB;
// Get records from the provided params.
$params = ['instanceid' => $instanceid];
$sql = "SELECT gc.shortname, gc.description, gc.maxscore, gf.score, gf.remark
FROM {gradingform_guide_fillings} gf
JOIN {gradingform_guide_criteria} gc ON gc.id = gf.criterionid
WHERE gf.instanceid = :instanceid";
$records = $DB->get_records_sql($sql, $params);
if ($records) {
$subcontext = array_merge($subcontext, [get_string('guide', 'gradingform_guide'), $instanceid]);
writer::with_context($context)->export_data($subcontext, (object) $records);
}
}
/**
* Deletes all user data related to the provided instance IDs.
*
* @param array $instanceids The instance IDs to delete information from.
*/
public static function delete_gradingform_for_instances(array $instanceids) {
global $DB;
$DB->delete_records_list('gradingform_guide_fillings', 'instanceid', $instanceids);
}
/**
* Store all user preferences for the plugin.
*
* @param int $userid The userid of the user whose data is to be exported.
*/
public static function export_user_preferences(int $userid) {
$prefvalue = get_user_preferences('gradingform_guide-showmarkerdesc', null, $userid);
if ($prefvalue !== null) {
$transformedvalue = transform::yesno($prefvalue);
writer::export_user_preference(
'gradingform_guide',
'gradingform_guide-showmarkerdesc',
$transformedvalue,
get_string('privacy:metadata:preference:showmarkerdesc', 'gradingform_guide')
);
}
$prefvalue = get_user_preferences('gradingform_guide-showstudentdesc', null, $userid);
if ($prefvalue !== null) {
$transformedvalue = transform::yesno($prefvalue);
writer::export_user_preference(
'gradingform_guide',
'gradingform_guide-showstudentdesc',
$transformedvalue,
get_string('privacy:metadata:preference:showstudentdesc', 'gradingform_guide')
);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="grade/grading/form/guide/db" VERSION="20120404" COMMENT="XMLDB file for Moodle marking guide"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="gradingform_guide_criteria" COMMENT="Stores the rows of the criteria grid.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="definitionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the form definition this criterion is part of"/>
<FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Defines the order of the criterion in the guide"/>
<FIELD NAME="shortname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="shortname of this criterion"/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The criterion description for students"/>
<FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the description field"/>
<FIELD NAME="descriptionmarkers" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Description for Markers"/>
<FIELD NAME="descriptionmarkersformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="maxscore" TYPE="number" LENGTH="10" NOTNULL="true" SEQUENCE="false" DECIMALS="5" COMMENT="maximum grade that can be assigned using this criterion"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_definitionid" TYPE="foreign" FIELDS="definitionid" REFTABLE="grading_definitions" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="gradingform_guide_fillings" COMMENT="Stores the data of how the guide is filled by a particular rater">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="instanceid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the grading form instance"/>
<FIELD NAME="criterionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the criterion (row) in the guide"/>
<FIELD NAME="remark" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Side note feedback regarding this particular criterion"/>
<FIELD NAME="remarkformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the remark field"/>
<FIELD NAME="score" TYPE="number" LENGTH="10" NOTNULL="true" SEQUENCE="false" DECIMALS="5" COMMENT="The score assigned"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_instanceid" TYPE="foreign" FIELDS="instanceid" REFTABLE="grading_instances" REFFIELDS="id"/>
<KEY NAME="fk_criterionid" TYPE="foreign" FIELDS="criterionid" REFTABLE="gradingform_guide_criteria" REFFIELDS="id"/>
<KEY NAME="uq_instance_criterion" TYPE="unique" FIELDS="instanceid, criterionid"/>
</KEYS>
</TABLE>
<TABLE NAME="gradingform_guide_comments" COMMENT="frequently used comments used in marking guide">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="definitionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the form definition this faq is part of"/>
<FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Defines the order of the comments"/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The comment description"/>
<FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the description field"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_definitionid" TYPE="foreign" FIELDS="definitionid" REFTABLE="grading_definitions" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
+41
View File
@@ -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/>.
/**
* 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',
'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',
'description' => 'Store the grading data for a user from the grader grading panel.',
'type' => 'write',
'ajax' => true,
],
];
+49
View File
@@ -0,0 +1,49 @@
<?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/>.
/**
* This file keeps track of upgrades to the marking guide grading method.
*
* @package gradingform_guide
* @category upgrade
* @copyright 2016 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Marking guide grading method upgrade task.
*
* @param int $oldversion The version we are upgrading form.
* @return bool Returns true on success.
* @throws coding_exception
* @throws downgrade_exception
* @throws upgrade_exception
*/
function xmldb_gradingform_guide_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
+66
View File
@@ -0,0 +1,66 @@
<?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 editor page
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__.'/../../../../config.php');
require_once(__DIR__.'/lib.php');
require_once(__DIR__.'/edit_form.php');
require_once($CFG->dirroot.'/grade/grading/lib.php');
$areaid = required_param('areaid', PARAM_INT);
$manager = get_grading_manager($areaid);
list($context, $course, $cm) = get_context_info_array($manager->get_context()->id);
require_login($course, true, $cm);
require_capability('moodle/grade:managegradingforms', $context);
$controller = $manager->get_controller('guide');
$PAGE->set_url(new moodle_url('/grade/grading/form/guide/edit.php', array('areaid' => $areaid)));
$PAGE->set_title(get_string('definemarkingguide', 'gradingform_guide'));
$PAGE->set_heading(get_string('definemarkingguide', 'gradingform_guide'));
$mform = new gradingform_guide_editguide(null, array('areaid' => $areaid, 'context' => $context,
'allowdraft' => !$controller->has_active_instances()), 'post', '', array('class' => 'gradingform_guide_editform'));
$data = $controller->get_definition_for_editing(true);
$returnurl = optional_param('returnurl', $manager->get_management_url(), PARAM_LOCALURL);
$data->returnurl = $returnurl;
$mform->set_data($data);
if ($mform->is_cancelled()) {
redirect($returnurl);
} else if ($mform->is_submitted() && $mform->is_validated() && !$mform->need_confirm_regrading($controller)) {
// Everything ok, validated, re-grading confirmed if needed. Make changes to the rubric.
$controller->update_definition($mform->get_data());
redirect($returnurl);
}
// Try to keep the session alive on this page as it may take some time
// before significant interaction happens with the server.
\core\session\manager::keepalive();
echo $OUTPUT->header();
$mform->display();
echo $OUTPUT->footer();
+223
View File
@@ -0,0 +1,223 @@
<?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/>.
/**
* The form used at the guide editor page is defined here
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/lib/formslib.php');
require_once(__DIR__.'/guideeditor.php');
MoodleQuickForm::registerElementType('guideeditor', $CFG->dirroot.'/grade/grading/form/guide/guideeditor.php',
'moodlequickform_guideeditor');
/**
* Defines the guide edit form
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class gradingform_guide_editguide extends moodleform {
/**
* Form element definition
*/
public function definition() {
$form = $this->_form;
$form->addElement('hidden', 'areaid');
$form->setType('areaid', PARAM_INT);
$form->addElement('hidden', 'returnurl');
$form->setType('returnurl', PARAM_LOCALURL);
// Name.
$form->addElement('text', 'name', get_string('name', 'gradingform_guide'),
array('size' => 52, 'maxlength' => 255));
$form->addRule('name', get_string('required'), 'required', null, 'client');
$form->setType('name', PARAM_TEXT);
$form->addRule('name', null, 'maxlength', 255, 'client');
// Description.
$options = gradingform_guide_controller::description_form_field_options($this->_customdata['context']);
$form->addElement('editor', 'description_editor', get_string('description'), null, $options);
$form->setType('description_editor', PARAM_RAW);
// Guide completion status.
$choices = array();
$choices[gradingform_controller::DEFINITION_STATUS_DRAFT] = html_writer::tag('span',
get_string('statusdraft', 'core_grading'), array('class' => 'status draft'));
$choices[gradingform_controller::DEFINITION_STATUS_READY] = html_writer::tag('span',
get_string('statusready', 'core_grading'), array('class' => 'status ready'));
$form->addElement('select', 'status', get_string('guidestatus', 'gradingform_guide'), $choices)->freeze();
// Guide editor.
$element = $form->addElement('guideeditor', 'guide', get_string('pluginname', 'gradingform_guide'));
$form->setType('guide', PARAM_RAW);
$buttonarray = array();
$buttonarray[] = &$form->createElement('submit', 'saveguide', get_string('saveguide', 'gradingform_guide'));
if ($this->_customdata['allowdraft']) {
$buttonarray[] = &$form->createElement('submit', 'saveguidedraft', get_string('saveguidedraft', 'gradingform_guide'));
}
$editbutton = &$form->createElement('submit', 'editguide', ' ');
$editbutton->freeze();
$buttonarray[] = &$editbutton;
$buttonarray[] = &$form->createElement('cancel');
$form->addGroup($buttonarray, 'buttonar', '', array(' '), false);
$form->closeHeaderBefore('buttonar');
}
/**
* Setup the form depending on current values. This method is called after definition(),
* data submission and set_data().
* All form setup that is dependent on form values should go in here.
*
* We remove the element status if there is no current status (i.e. guide is only being created)
* so the users do not get confused
*/
public function definition_after_data() {
$form = $this->_form;
$el = $form->getElement('status');
if (!$el->getValue()) {
$form->removeElement('status');
} else {
$vals = array_values($el->getValue());
if ($vals[0] == gradingform_controller::DEFINITION_STATUS_READY) {
$this->findbutton('saveguide')->setValue(get_string('save', 'gradingform_guide'));
}
}
}
/**
* Form vlidation.
* If there are errors return array of errors ("fieldname"=>"error message"),
* otherwise true if ok.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
$err = parent::validation($data, $files);
$err = array();
$form = $this->_form;
$guideel = $form->getElement('guide');
if ($guideel->non_js_button_pressed($data['guide'])) {
// If JS is disabled and button such as 'Add criterion' is pressed - prevent from submit.
$err['guidedummy'] = 1;
} else if (isset($data['editguide'])) {
// Continue editing.
$err['guidedummy'] = 1;
} else if ((isset($data['saveguide']) && $data['saveguide']) ||
(isset($data['saveguidedraft']) && $data['saveguidedraft'])) {
// If user attempts to make guide active - it needs to be validated.
if ($guideel->validate($data['guide']) !== false) {
$err['guidedummy'] = 1;
}
}
return $err;
}
/**
* Return submitted data if properly submitted or returns NULL if validation fails or
* if there is no submitted data.
*
* @return object submitted data; NULL if not valid or not submitted or cancelled
*/
public function get_data() {
$data = parent::get_data();
if (!empty($data->saveguide)) {
$data->status = gradingform_controller::DEFINITION_STATUS_READY;
} else if (!empty($data->saveguidedraft)) {
$data->status = gradingform_controller::DEFINITION_STATUS_DRAFT;
}
return $data;
}
/**
* Check if there are changes in the guide and it is needed to ask user whether to
* mark the current grades for re-grading. User may confirm re-grading and continue,
* return to editing or cancel the changes
*
* @param gradingform_guide_controller $controller
*/
public function need_confirm_regrading($controller) {
$data = $this->get_data();
if (isset($data->guide['regrade'])) {
// We have already displayed the confirmation on the previous step.
return false;
}
if (!isset($data->saveguide) || !$data->saveguide) {
// We only need confirmation when button 'Save guide' is pressed.
return false;
}
if (!$controller->has_active_instances()) {
// Nothing to re-grade, confirmation not needed.
return false;
}
$changelevel = $controller->update_or_check_guide($data);
if ($changelevel == 0) {
// No changes in the guide, no confirmation needed.
return false;
}
// Freeze form elements and pass the values in hidden fields.
// TODO description_editor does not freeze the normal way!
$form = $this->_form;
foreach (array('guide', 'name'/*, 'description_editor'*/) as $fieldname) {
$el =& $form->getElement($fieldname);
$el->freeze();
$el->setPersistantFreeze(true);
if ($fieldname == 'guide') {
$el->add_regrade_confirmation($changelevel);
}
}
// Replace button text 'saveguide' and unfreeze 'Back to edit' button.
$this->findbutton('saveguide')->setValue(get_string('continue'));
$el =& $this->findbutton('editguide');
$el->setValue(get_string('backtoediting', 'gradingform_guide'));
$el->unfreeze();
return true;
}
/**
* Returns a form element (submit button) with the name $elementname
*
* @param string $elementname
* @return HTML_QuickForm_element
*/
protected function &findbutton($elementname) {
$form = $this->_form;
$buttonar =& $form->getElement('buttonar');
$elements =& $buttonar->getElements();
foreach ($elements as $el) {
if ($el->getName() == $elementname) {
return $el;
}
}
return null;
}
}
+374
View File
@@ -0,0 +1,374 @@
<?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/>.
/**
* This file contains the marking guide editor element
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once("HTML/QuickForm/input.php");
/**
* The editor for the marking guide advanced grading plugin.
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodlequickform_guideeditor extends HTML_QuickForm_input {
/** @var string help message */
public $_helpbutton = '';
/** @var null|false|string stores the result of the last validation: null - undefined, false - no errors,
* string - error(s) text */
protected $validationerrors = null;
/** @var bool if element has already been validated **/
protected $wasvalidated = false;
/** @var null|bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */
protected $nonjsbuttonpressed = false;
/** @var string|false Message to display in front of the editor (that there exist grades on this guide being edited) */
protected $regradeconfirmation = false;
/**
* Constructor
*
* @param string $elementname
* @param string $elementlabel
* @param array $attributes
*/
public function __construct($elementname=null, $elementlabel=null, $attributes=null) {
parent::__construct($elementname, $elementlabel, $attributes);
}
/**
* Old syntax of class constructor. Deprecated in PHP7.
*
* @deprecated since Moodle 3.1
*/
public function moodlequickform_guideeditor($elementname=null, $elementlabel=null, $attributes=null) {
debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
self::__construct($elementname, $elementlabel, $attributes);
}
/**
* get html for help button
*
* @return string html for help button
*/
public function getHelpButton() {
return $this->_helpbutton;
}
/**
* The renderer will take care itself about different display in normal and frozen states
*
* @return string
*/
public function getElementTemplateType() {
return 'default';
}
/**
* Specifies that confirmation about re-grading needs to be added to this rubric editor.
* $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml()
*
* @see gradingform_rubric_controller::update_or_check_rubric()
* @param int $changelevel
*/
public function add_regrade_confirmation($changelevel) {
$this->regradeconfirmation = $changelevel;
}
/**
* Returns html string to display this element
*
* @return string
*/
public function toHtml() {
global $PAGE;
$html = $this->_getTabs();
$renderer = $PAGE->get_renderer('gradingform_guide');
$data = $this->prepare_data(null, $this->wasvalidated);
if (!$this->_flagFrozen) {
$mode = gradingform_guide_controller::DISPLAY_EDIT_FULL;
$module = array('name'=>'gradingform_guideeditor',
'fullpath'=>'/grade/grading/form/guide/js/guideeditor.js',
'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
'strings' => array(
array('confirmdeletecriterion', 'gradingform_guide'),
array('clicktoedit', 'gradingform_guide'),
array('clicktoeditname', 'gradingform_guide')
));
$PAGE->requires->js_init_call('M.gradingform_guideeditor.init', array(
array('name' => $this->getName(),
'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
'commenttemplate' => $renderer->comment_template($mode, $this->getName())
)),
true, $module);
} else {
// Guide is frozen, no javascript needed.
if ($this->_persistantFreeze) {
$mode = gradingform_guide_controller::DISPLAY_EDIT_FROZEN;
} else {
$mode = gradingform_guide_controller::DISPLAY_PREVIEW;
}
}
if ($this->regradeconfirmation) {
if (!isset($data['regrade'])) {
$data['regrade'] = 1;
}
$html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
}
if ($this->validationerrors) {
$html .= html_writer::div($renderer->notification($this->validationerrors));
}
$html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
return $html;
}
/**
* Prepares the data passed in $_POST:
* - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled)
* sets $this->nonjsbuttonpressed to true/false if such button was pressed
* - if options not passed (i.e. we create a new guide) fills the options array with the default values
* - if options are passed completes the options array with unchecked checkboxes
* - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string
* and stores it in $this->validationerrors
*
* @param array $value
* @param boolean $withvalidation whether to enable data validation
* @return array
*/
protected function prepare_data($value = null, $withvalidation = false) {
if (null === $value) {
$value = $this->getValue();
}
if ($this->nonjsbuttonpressed === null) {
$this->nonjsbuttonpressed = false;
}
$errors = array();
$return = array('criteria' => array(), 'options' => gradingform_guide_controller::get_default_options(),
'comments' => array());
if (!isset($value['criteria'])) {
$value['criteria'] = array();
$errors['err_nocriteria'] = 1;
}
// If options are present in $value, replace default values with submitted values.
if (!empty($value['options'])) {
foreach (array_keys($return['options']) as $option) {
// Special treatment for checkboxes.
if (!empty($value['options'][$option])) {
$return['options'][$option] = $value['options'][$option];
} else {
$return['options'][$option] = null;
}
}
}
if (is_array($value)) {
// For other array keys of $value no special treatmeant neeeded, copy them to return value as is.
foreach (array_keys($value) as $key) {
if ($key != 'options' && $key != 'criteria' && $key != 'comments') {
$return[$key] = $value[$key];
}
}
}
// Iterate through criteria.
$lastaction = null;
$lastid = null;
foreach ($value['criteria'] as $id => $criterion) {
if ($id == 'addcriterion') {
$id = $this->get_next_id(array_keys($value['criteria']));
$criterion = array('description' => '');
$this->nonjsbuttonpressed = true;
}
if ($withvalidation && !array_key_exists('delete', $criterion)) {
if (!strlen(trim($criterion['shortname']))) {
$errors['err_noshortname'] = 1;
$criterion['error_description'] = true;
}
if (strlen(trim($criterion['shortname'])) > 255) {
$errors['err_shortnametoolong'] = 1;
$criterion['error_description'] = true;
}
if (!strlen(trim($criterion['maxscore']))) {
$errors['err_nomaxscore'] = 1;
$criterion['error_description'] = true;
} else if (!is_numeric($criterion['maxscore'])) {
$errors['err_maxscorenotnumeric'] = 1;
$criterion['error_description'] = true;
} else if ($criterion['maxscore'] < 0) {
$errors['err_maxscoreisnegative'] = 1;
$criterion['error_description'] = true;
}
}
if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
unset($criterion['moveup']);
if ($lastid !== null) {
$lastcriterion = $return['criteria'][$lastid];
unset($return['criteria'][$lastid]);
$return['criteria'][$id] = $criterion;
$return['criteria'][$lastid] = $lastcriterion;
} else {
$return['criteria'][$id] = $criterion;
}
$lastaction = null;
$lastid = $id;
$this->nonjsbuttonpressed = true;
} else if (array_key_exists('delete', $criterion)) {
$this->nonjsbuttonpressed = true;
} else {
if (array_key_exists('movedown', $criterion)) {
unset($criterion['movedown']);
$lastaction = 'movedown';
$this->nonjsbuttonpressed = true;
}
$return['criteria'][$id] = $criterion;
$lastid = $id;
}
}
// Add sort order field to criteria.
$csortorder = 1;
foreach (array_keys($return['criteria']) as $id) {
$return['criteria'][$id]['sortorder'] = $csortorder++;
}
// Iterate through comments.
$lastaction = null;
$lastid = null;
if (!empty($value['comments'])) {
foreach ($value['comments'] as $id => $comment) {
if ($id == 'addcomment') {
$id = $this->get_next_id(array_keys($value['comments']));
$comment = array('description' => '');
$this->nonjsbuttonpressed = true;
}
if (array_key_exists('moveup', $comment) || $lastaction == 'movedown') {
unset($comment['moveup']);
if ($lastid !== null) {
$lastcomment = $return['comments'][$lastid];
unset($return['comments'][$lastid]);
$return['comments'][$id] = $comment;
$return['comments'][$lastid] = $lastcomment;
} else {
$return['comments'][$id] = $comment;
}
$lastaction = null;
$lastid = $id;
$this->nonjsbuttonpressed = true;
} else if (array_key_exists('delete', $comment)) {
$this->nonjsbuttonpressed = true;
} else {
if (array_key_exists('movedown', $comment)) {
unset($comment['movedown']);
$lastaction = 'movedown';
$this->nonjsbuttonpressed = true;
}
$return['comments'][$id] = $comment;
$lastid = $id;
}
}
// Add sort order field to comments.
$csortorder = 1;
foreach (array_keys($return['comments']) as $id) {
$return['comments'][$id]['sortorder'] = $csortorder++;
}
}
// Create validation error string (if needed).
if ($withvalidation) {
if (count($errors)) {
$rv = array();
foreach ($errors as $error => $v) {
$rv[] = get_string($error, 'gradingform_guide');
}
$this->validationerrors = join('<br/ >', $rv);
} else {
$this->validationerrors = false;
}
$this->wasvalidated = true;
}
return $return;
}
/**
* Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns
*
* @param array $ids
* @return string
*/
protected function get_next_id($ids) {
$maxid = 0;
foreach ($ids as $id) {
if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) {
$maxid = (int)$matches[1];
}
}
return 'NEWID'.($maxid+1);
}
/**
* Checks if a submit button was pressed which is supposed to be processed on client side by JS
* but user seem to have disabled JS in the browser.
* (buttons 'add criteria', 'add level', 'move up', 'move down', 'add comment')
* In this case the form containing this element is prevented from being submitted
*
* @param array $value
* @return boolean true if non-submit button was pressed and not processed by JS
*/
public function non_js_button_pressed($value) {
if ($this->nonjsbuttonpressed === null) {
$this->prepare_data($value);
}
return $this->nonjsbuttonpressed;
}
/**
* Validates that guide has at least one criterion, filled definitions and all criteria
* have filled descriptions
*
* @param array $value
* @return string|false error text or false if no errors found
*/
public function validate($value) {
if (!$this->wasvalidated) {
$this->prepare_data($value, true);
}
return $this->validationerrors;
}
/**
* Prepares the data for saving
* @see prepare_data()
*
* @param array $submitvalues
* @param boolean $assoc
* @return array
*/
public function exportValue(&$submitvalues, $assoc = false) {
$value = $this->prepare_data($this->_findValue($submitvalues));
return $this->_prepareValue($value, $assoc);
}
}
+32
View File
@@ -0,0 +1,32 @@
M.gradingform_guide = {};
/**
* This function is called for each guide on page.
*/
M.gradingform_guide.init = function(Y, options) {
var currentfocus = Y.one('.markingguideremark');
Y.all('.markingguideremark').on('blur', function(e) {
currentfocus = e.currentTarget;
});
Y.all('.markingguidecomment').on('click', function(e) {
currentfocus.set('value', currentfocus.get('value') + '\n' + e.currentTarget.get('text'));
currentfocus.focus();
});
Y.all('.showmarkerdesc input[type=radio]').on('click', function(e) {
if (e.currentTarget.get('value')=='false') {
Y.all('.criteriondescriptionmarkers').addClass('hide');
} else {
Y.all('.criteriondescriptionmarkers').removeClass('hide');
}
});
Y.all('.showstudentdesc input[type=radio]').on('click', function(e) {
if (e.currentTarget.get('value')=='false') {
Y.all('.criteriondescription').addClass('hide');
} else {
Y.all('.criteriondescription').removeClass('hide');
}
});
};
+267
View File
@@ -0,0 +1,267 @@
M.gradingform_guideeditor = {'templates' : {}, 'eventhandler' : null, 'name' : null, 'Y' : null};
/**
* This function is called for each guideeditor on page.
*/
M.gradingform_guideeditor.init = function(Y, options) {
M.gradingform_guideeditor.name = options.name
M.gradingform_guideeditor.Y = Y
M.gradingform_guideeditor.templates[options.name] = {
'criterion' : options.criteriontemplate,
'comment' : options.commenttemplate
}
M.gradingform_guideeditor.disablealleditors()
Y.on('click', M.gradingform_guideeditor.clickanywhere, 'body', null)
YUI().use('event-touch', function (Y) {
Y.one('body').on('touchstart', M.gradingform_guideeditor.clickanywhere);
Y.one('body').on('touchend', M.gradingform_guideeditor.clickanywhere);
})
M.gradingform_guideeditor.addhandlers()
};
// Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html
M.gradingform_guideeditor.addhandlers = function() {
var Y = M.gradingform_guideeditor.Y
var name = M.gradingform_guideeditor.name
if (M.gradingform_guideeditor.eventhandler) {
M.gradingform_guideeditor.eventhandler.detach()
}
M.gradingform_guideeditor.eventhandler = Y.on('click', M.gradingform_guideeditor.buttonclick, '#guide-'+name+' input[type=submit]', null);
}
// switches all input text elements to non-edit mode
M.gradingform_guideeditor.disablealleditors = function() {
var Y = M.gradingform_guideeditor.Y
var name = M.gradingform_guideeditor.name
Y.all('#guide-'+name+' .criteria .description input[type=text]:not(.pseudotablink)').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} );
Y.all('#guide-'+name+' .criteria .description textarea').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} );
Y.all('#guide-'+name+' .comments .description textarea').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} );
}
// function invoked on each click on the page. If criterion values are clicked
// it switches the element to edit mode. If guide button is clicked it does nothing so the 'buttonclick'
// function is invoked
M.gradingform_guideeditor.clickanywhere = function(e) {
if (e.type == 'touchstart') {
return
}
var el = e.target
// if clicked on button - disablecurrenteditor, continue
if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') {
return
}
// if clicked on description item and this item is not enabled - enable it
var container = null
if ((container = el.ancestor('.criterionname')) || (container = el.ancestor('.criterionmaxscore'))) {
el = container.one('input[type=text]')
} else if ((container = el.ancestor('.criteriondesc')) || (container = el.ancestor('.criteriondescmarkers'))) {
el = container.one('textarea')
} else {
el = null
}
if (el) {
if (el.hasClass('hiddenelement')) {
M.gradingform_guideeditor.disablealleditors()
M.gradingform_guideeditor.editmode(el, true)
}
return
}
// else disablecurrenteditor
M.gradingform_guideeditor.disablealleditors()
}
// switch the criterion item to edit mode or switch back
M.gradingform_guideeditor.editmode = function(el, editmode) {
var Y = M.gradingform_guideeditor.Y
var ta = el
if (!editmode && ta.hasClass('hiddenelement')) {
return;
}
if (editmode && !ta.hasClass('hiddenelement')) {
return;
}
var pseudotablink = '<span class="pseudotablink" tabindex="0"></span>',
taplain = ta.next('.plainvalue'),
tbplain = null,
tb = el.one('.score input[type=text]')
// add 'plainvalue' next to textarea for description/definition and next to input text field for score (if applicable)
if (!taplain && ta.get('name') != '') {
ta.insert('<div class="plainvalue">'+pseudotablink+'<span class="textvalue">&nbsp;</span></div>', 'after')
taplain = ta.next('.plainvalue')
taplain.one('.pseudotablink').on('focus', M.gradingform_guideeditor.clickanywhere)
if (tb) {
tb.get('parentNode').append('<span class="plainvalue">'+pseudotablink+'<span class="textvalue">&nbsp;</span></span>')
tbplain = tb.get('parentNode').one('.plainvalue')
tbplain.one('.pseudotablink').on('focus', M.gradingform_guideeditor.clickanywhere)
}
}
if (tb && !tbplain) {
tbplain = tb.get('parentNode').one('.plainvalue')
}
if (!editmode) {
// if we need to hide the input fields, copy their contents to plainvalue(s). If description/definition
// is empty, display the default text ('Click to edit ...') and add/remove 'empty' CSS class to element
var value = Y.Lang.trim(ta.get('value'));
if (value.length) {
taplain.removeClass('empty')
} else if (ta.get('name').indexOf('[shortname]') > 1){
value = M.util.get_string('clicktoeditname', 'gradingform_guide')
taplain.addClass('editname')
} else {
value = M.util.get_string('clicktoedit', 'gradingform_guide')
taplain.addClass('empty')
}
// Replace newlines with <br> tags, when displaying in the page.
taplain.one('.textvalue').set('innerHTML', Y.Escape.html(value).replace(/(?:\r\n|\r|\n)/g, '<br>'))
if (tb) {
tbplain.one('.textvalue').set('innerHTML', Y.Escape.html(tb.get('value')))
}
// hide/display textarea, textbox and plaintexts
taplain.removeClass('hiddenelement')
ta.addClass('hiddenelement')
if (tb) {
tbplain.removeClass('hiddenelement')
tb.addClass('hiddenelement')
}
} else {
// if we need to show the input fields, set the width/height for textarea so it fills the cell
try {
if (ta.get('name').indexOf('[maxscore]') > 1) {
ta.setStyle('width', '25px');
} else {
var width = parseFloat(ta.get('parentNode').getComputedStyle('width'))-10,
height = parseFloat(ta.get('parentNode').getComputedStyle('height'))
ta.setStyle('width', Math.max(width,50)+'px')
ta.setStyle('height', Math.max(height,30)+'px')
}
}
catch (err) {
// this browser do not support 'computedStyle', leave the default size of the textbox
}
// hide/display textarea, textbox and plaintexts
taplain.addClass('hiddenelement')
ta.removeClass('hiddenelement')
if (tb) {
tbplain.addClass('hiddenelement')
tb.removeClass('hiddenelement')
}
}
// focus the proper input field in edit mode
if (editmode) {
ta.focus()
}
}
// handler for clicking on submit buttons within guideeditor element. Adds/deletes/rearranges criteria/comments on client side
M.gradingform_guideeditor.buttonclick = function(e, confirmed) {
var Y = M.gradingform_guideeditor.Y
var name = M.gradingform_guideeditor.name
if (e.target.get('type') != 'submit') {
return;
}
M.gradingform_guideeditor.disablealleditors()
var chunks = e.target.get('id').split('-')
var section = chunks[1]
var action = chunks[chunks.length-1]
if (chunks[0] != name || (section != 'criteria' && section != 'comments')) {
return;
}
// prepare the id of the next inserted criterion
var elements_str;
if (section == 'criteria') {
elements_str = '#guide-'+name+' .criteria .criterion'
} else if (section == 'comments') {
elements_str = '#guide-'+name+' .comments .criterion'
}
var newid = 0;
if (action == 'addcriterion' || action == 'addcomment') {
newid = M.gradingform_guideeditor.calculatenewid(elements_str);
}
if (chunks.length == 3 && (action == 'addcriterion' || action == 'addcomment')) {
// ADD NEW CRITERION OR COMMENT
var parentel = Y.one('#'+name+'-'+section)
if (parentel.one('>tbody')) {
parentel = parentel.one('>tbody')
}
if (section == 'criteria') {
var newcriterion = M.gradingform_guideeditor.templates[name]['criterion']
parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''))
} else if (section == 'comments') {
var newcomment = M.gradingform_guideeditor.templates[name]['comment']
parentel.append(newcomment.replace(/\{COMMENT-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''))
}
M.gradingform_guideeditor.addhandlers();
M.gradingform_guideeditor.disablealleditors()
M.gradingform_guideeditor.assignclasses(elements_str)
// Enable edit mode of the newly added criterion/comment entry.
var inputTarget = 'shortname';
if (action == 'addcomment') {
inputTarget = 'description';
}
var inputTargetId = '#guide-' + name + ' #' + name + '-' + section + '-NEWID' + newid + '-' + inputTarget;
M.gradingform_guideeditor.editmode(Y.one(inputTargetId), true);
} else if (chunks.length == 4 && action == 'moveup') {
// MOVE UP
el = Y.one('#'+name+'-'+section+'-'+chunks[2])
if (el.previous()) {
el.get('parentNode').insertBefore(el, el.previous())
}
M.gradingform_guideeditor.assignclasses(elements_str)
} else if (chunks.length == 4 && action == 'movedown') {
// MOVE DOWN
el = Y.one('#'+name+'-'+section+'-'+chunks[2])
if (el.next()) {
el.get('parentNode').insertBefore(el.next(), el)
}
M.gradingform_guideeditor.assignclasses(elements_str)
} else if (chunks.length == 4 && action == 'delete') {
// DELETE
if (confirmed) {
Y.one('#'+name+'-'+section+'-'+chunks[2]).remove()
M.gradingform_guideeditor.assignclasses(elements_str)
} else {
require(['core/notification', 'core/str'], function(Notification, Str) {
Notification.saveCancelPromise(
Str.get_string('confirmation', 'admin'),
Str.get_string('confirmdeletecriterion', 'gradingform_guide'),
Str.get_string('yes', 'moodle')
).then(function() {
M.gradingform_guideeditor.buttonclick.apply(this, [e, true]);
return;
}.bind(this)).catch(function() {
// User cancelled.
});
}.bind(this));
}
} else {
// unknown action
return;
}
e.preventDefault();
}
// properly set classes (first/last/odd/even) and/or criterion sortorder for elements Y.all(elements_str)
M.gradingform_guideeditor.assignclasses = function (elements_str) {
var elements = M.gradingform_guideeditor.Y.all(elements_str)
for (var i=0; i<elements.size(); i++) {
elements.item(i).removeClass('first').removeClass('last').removeClass('even').removeClass('odd').
addClass(((i%2)?'odd':'even') + ((i==0)?' first':'') + ((i==elements.size()-1)?' last':''))
elements.item(i).all('input[type=hidden]').each(
function(node) {if (node.get('name').match(/sortorder/)) { node.set('value', i)}}
);
}
}
// returns unique id for the next added element, it should not be equal to any of Y.all(elements_str) ids
M.gradingform_guideeditor.calculatenewid = function (elements_str) {
var newid = 1
M.gradingform_guideeditor.Y.all(elements_str).each( function(node) {
var idchunks = node.get('id').split('-'), id = idchunks.pop();
if (id.match(/^NEWID(\d+)$/)) { newid = Math.max(newid, parseInt(id.substring(5))+1)};
} );
return newid
}
@@ -0,0 +1,104 @@
<?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/>.
/**
* Strings for the marking guide advanced grading plugin
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['addcomment'] = 'Add frequently used comment';
$string['additionalcomments'] = 'Additional comments';
$string['additionalcommentsforcriterion'] = 'Additional comments for criterion, {$a}';
$string['addcriterion'] = 'Add criterion';
$string['alwaysshowdefinition'] = 'Show guide definition to students';
$string['backtoediting'] = 'Back to editing';
$string['clicktocopy'] = 'Click to copy this text into the criteria feedback';
$string['clicktoedit'] = 'Click to edit';
$string['clicktoeditname'] = 'Click to edit criterion name';
$string['comment'] = 'Comment';
$string['commentpickerforcriterion'] = 'Frequently used comments picker for {$a} additional comments';
$string['comments'] = 'Frequently used comments';
$string['commentsdelete'] = 'Delete comment';
$string['commentsempty'] = 'Click to edit comment';
$string['commentsmovedown'] = 'Move down';
$string['commentsmoveup'] = 'Move up';
$string['confirmdeletecriterion'] = 'Are you sure you want to delete this item?';
$string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
$string['criterion'] = 'Criterion name';
$string['criteriondelete'] = 'Delete criterion';
$string['criterionempty'] = 'Click to edit criterion';
$string['criterionmovedown'] = 'Move down';
$string['criterionmoveup'] = 'Move up';
$string['criterionname'] = 'Criterion name';
$string['criterionremark'] = '{$a} criterion remark';
$string['definemarkingguide'] = 'Define marking guide';
$string['description'] = 'Description';
$string['descriptionmarkers'] = 'Description for Markers';
$string['descriptionstudents'] = 'Description for students';
$string['err_maxscoreisnegative'] = 'The max score is not valid, negative values are not allowed';
$string['err_maxscorenotnumeric'] = 'Criterion max score must be numeric';
$string['err_nocomment'] = 'Comment can not be empty';
$string['err_nodescription'] = 'Student description can not be empty';
$string['err_nodescriptionmarkers'] = 'Marker description can not be empty';
$string['err_nomaxscore'] = 'Criterion max score can not be empty';
$string['err_noshortname'] = 'Criterion name can not be empty';
$string['err_shortnametoolong'] = 'Criterion name must be less than 256 characters';
$string['err_scoreinvalid'] = 'The score given to \'{$a->criterianame}\' is not valid, the max score is: {$a->maxscore}';
$string['err_scoreisnegative'] = 'The score given to \'{$a->criterianame}\' is not valid, negative values are not allowed';
$string['gradingof'] = '{$a} grading';
$string['guide'] = 'Marking guide';
$string['guidemappingexplained'] = 'WARNING: Your marking guide has a maximum grade of <b>{$a->maxscore} points</b> but the maximum grade set in your activity is {$a->modulegrade} The maximum score set in your marking guide will be scaled to the maximum grade in the module.<br />
Intermediate scores will be converted respectively and rounded to the nearest available grade.';
$string['guidenotcompleted'] = 'Please provide a valid grade for each criterion';
$string['guideoptions'] = 'Marking guide options';
$string['guidestatus'] = 'Current marking guide status';
$string['hidemarkerdesc'] = 'Hide marker criterion descriptions';
$string['hidestudentdesc'] = 'Hide student criterion descriptions';
$string['informationforcriterion'] = '{$a} information';
$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'] = 'Score out of {$a}';
$string['pluginname'] = 'Marking guide';
$string['previewmarkingguide'] = 'Preview marking guide';
$string['privacy:metadata:criterionid'] = 'An identifier to a criterion for advanced marking.';
$string['privacy:metadata:fillingssummary'] = 'Stores information about a user\'s grade and feedback for the marking guide.';
$string['privacy:metadata:instanceid'] = 'An identifier to a grade used by an activity.';
$string['privacy:metadata:preference:showmarkerdesc'] = 'Whether to show marker criterion descriptions';
$string['privacy:metadata:preference:showstudentdesc'] = 'Whether to show student criterion descriptions';
$string['privacy:metadata:remark'] = 'Remarks related to this grade criterion.';
$string['privacy:metadata:score'] = 'A score for this grade criterion.';
$string['regrademessage1'] = 'You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded.';
$string['regrademessage5'] = 'You are about to save significant changes to a marking guide that has already been used for grading. The gradebook value will be unchanged, but the marking guide will be hidden from students until their item is regraded.';
$string['regradeoption0'] = 'Do not mark for regrade';
$string['regradeoption1'] = 'Mark for regrade';
$string['remark_help'] = 'Enter any additional comments about this criterion.';
$string['restoredfromdraft'] = 'NOTE: The last attempt to grade this person was not saved properly so draft grades have been restored. If you want to cancel these changes use the \'Cancel\' button below.';
$string['save'] = 'Save';
$string['saveguide'] = 'Save marking guide and make it ready';
$string['saveguidedraft'] = 'Save as draft';
$string['score'] = 'score';
$string['scoreforcriterion'] = '{$a} score';
$string['score_help'] = 'Enter a score for {$a->criterion} between 0 and {$a->maxscore}.';
$string['showmarkerdesc'] = 'Show marker criterion descriptions';
$string['showmarkspercriterionstudents'] = 'Show marks per criterion to students';
$string['showstudentdesc'] = 'Show student criterion descriptions';
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

+3
View File
@@ -0,0 +1,3 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M15 0H1C.5 0 0 .5 0 1v14c0 .5.5 1 1 1h14c.5 0 1-.5 1-1V1c0-.5-.5-1-1-1zM8 14H2v-2h6v2zm0-3H2V9h6v2zm0-3H2V6h6v2zm3 6H9v-2h2v2zm0-3H9V9h2v2zm0-3H9V6h2v2zm3 6h-2v-2h2v2zm0-3h-2V9h2v2zm0-3h-2V6h2v2z" fill="#888"/></svg>

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

+3
View File
@@ -0,0 +1,3 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><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="#888"/></svg>

After

Width:  |  Height:  |  Size: 583 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

+3
View File
@@ -0,0 +1,3 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><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="#888"/></svg>

After

Width:  |  Height:  |  Size: 479 B

+53
View File
@@ -0,0 +1,53 @@
<?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/>.
/**
* Preview marking guide page
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__.'/../../../../config.php');
require_once(__DIR__.'/lib.php');
require_once(__DIR__.'/edit_form.php');
require_once($CFG->dirroot.'/grade/grading/lib.php');
$areaid = required_param('areaid', PARAM_INT);
$manager = get_grading_manager($areaid);
list($context, $course, $cm) = get_context_info_array($manager->get_context()->id);
require_login($course, true, $cm);
$controller = $manager->get_controller('guide');
$options = $controller->get_options();
if (!$controller->is_form_defined() || empty($options['alwaysshowdefinition'])) {
throw new moodle_exception('nopermissions', 'error', '', get_string('previewmarkingguide', 'gradingform_guide'));
}
$title = get_string('gradingof', 'gradingform_guide', $manager->get_area_title());
$PAGE->set_url(new moodle_url('/grade/grading/form/guide/preview.php', array('areaid' => $areaid)));
$PAGE->set_title($title);
$PAGE->set_heading($title);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
echo $controller->render_preview($PAGE);
echo $OUTPUT->footer();
+793
View File
@@ -0,0 +1,793 @@
<?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/>.
/**
* Contains the Guide grading form renderer in all of its glory
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Grading method plugin renderer
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class gradingform_guide_renderer extends plugin_renderer_base {
/**
* This function returns html code for displaying criterion. Depending on $mode it may be the
* code to edit guide, to preview the guide, to evaluate somebody or to review the evaluation.
*
* This function may be called from display_guide() to display the whole guide, or it can be
* called by itself to return a template used by JavaScript to add new empty criteria to the
* guide being designed.
* In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc.
*
* When overriding this function it is very important to remember that all elements of html
* form (in edit or evaluate mode) must have the name $elementname.
*
* Also JavaScript relies on the class names of elements and when developer changes them
* script might stop working.
*
* @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller()}
* @param array $options An array of options.
* showmarkspercriterionstudents (bool) If true adds the current score to the display
* @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
* @param array $criterion criterion data
* @param array $value (only in view mode) teacher's feedback on this criterion
* @param array $validationerrors An array containing validation errors to be shown
* @param array $comments Array of frequently used comments.
* @return string
*/
public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $value = null,
$validationerrors = null, $comments = null) {
if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) {
$criterion = array('id' => '{CRITERION-id}',
'description' => '{CRITERION-description}',
'sortorder' => '{CRITERION-sortorder}',
'class' => '{CRITERION-class}',
'descriptionmarkers' => '{CRITERION-descriptionmarkers}',
'shortname' => '{CRITERION-shortname}',
'maxscore' => '{CRITERION-maxscore}');
} else {
foreach (array('sortorder', 'description', 'class', 'shortname', 'descriptionmarkers', 'maxscore') as $key) {
// Set missing array elements to empty strings to avoid warnings.
if (!array_key_exists($key, $criterion)) {
$criterion[$key] = '';
}
}
}
$criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'],
'id' => '{NAME}-criteria-{CRITERION-id}'));
$descriptionclass = 'description';
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
$criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls'));
foreach (array('moveup', 'delete', 'movedown') as $key) {
$value = get_string('criterion'.$key, 'gradingform_guide');
$button = html_writer::empty_tag('input', array('type' => 'submit',
'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']',
'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value));
$criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
}
$criteriontemplate .= html_writer::end_tag('td'); // Controls.
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
$shortnameinput = html_writer::empty_tag('input', array('type' => 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][shortname]',
'id ' => '{NAME}-criteria-{CRITERION-id}-shortname',
'value' => $criterion['shortname'],
'aria-labelledby' => '{NAME}-criterion-name-label'));
$shortname = html_writer::tag('div', $shortnameinput, array('class' => 'criterionname'));
$descriptioninput = html_writer::tag('textarea', s($criterion['description']),
array('name' => '{NAME}[criteria][{CRITERION-id}][description]',
'id' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '65', 'rows' => '5'));
$description = html_writer::tag('div', $descriptioninput, array('class' => 'criteriondesc'));
$descriptionmarkersinput = html_writer::tag('textarea', s($criterion['descriptionmarkers']),
array('name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]',
'id' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'cols' => '65', 'rows' => '5'));
$descriptionmarkers = html_writer::tag('div', $descriptionmarkersinput, array('class' => 'criteriondescmarkers'));
$maxscore = html_writer::empty_tag('input', array('type'=> 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'size' => '3',
'value' => $criterion['maxscore'],
'id' => '{NAME}[criteria][{CRITERION-id}][maxscore]'));
$maxscore = html_writer::tag('div', $maxscore, array('class'=>'criterionmaxscore'));
} else {
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'value' => $criterion['shortname']));
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'value' => $criterion['description']));
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'value' => $criterion['descriptionmarkers']));
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'value' => $criterion['maxscore']));
} else if ($mode == gradingform_guide_controller::DISPLAY_EVAL ||
$mode == gradingform_guide_controller::DISPLAY_VIEW) {
$descriptionclass = 'descriptionreadonly';
}
$shortnameparams = array(
'name' => '{NAME}[criteria][{CRITERION-id}][shortname]',
'id' => '{NAME}[criteria][{CRITERION-id}][shortname]',
'aria-describedby' => '{NAME}-criterion-name-label'
);
$shortname = html_writer::div(
format_text($criterion['shortname'], FORMAT_HTML),
'criterionshortname',
$shortnameparams
);
$descmarkerclass = '';
$descstudentclass = '';
if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
if (!get_user_preferences('gradingform_guide-showmarkerdesc', true)) {
$descmarkerclass = ' hide';
}
if (!get_user_preferences('gradingform_guide-showstudentdesc', true)) {
$descstudentclass = ' hide';
}
}
$description = html_writer::tag('div',
format_text($criterion['description'], $criterion['descriptionformat']),
array('class'=>'criteriondescription'.$descstudentclass,
'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]'));
$descriptionmarkers = html_writer::tag('div',
format_text($criterion['descriptionmarkers'], $criterion['descriptionmarkersformat']),
array('class'=>'criteriondescriptionmarkers'.$descmarkerclass,
'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]'));
$maxscore = html_writer::tag('div', s($criterion['maxscore']),
array('class'=>'criteriondescriptionscore', 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]'));
// Retain newlines as <br> tags when displaying the marking guide.
$description = nl2br($description);
$descriptionmarkers = nl2br($descriptionmarkers);
}
if (isset($criterion['error_description'])) {
$descriptionclass .= ' error';
}
$title = $shortname;
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL ||
$mode == gradingform_guide_controller::DISPLAY_PREVIEW) {
$title .= html_writer::tag('label', get_string('descriptionstudents', 'gradingform_guide'),
array('for'=>'{NAME}[criteria][{CRITERION-id}][description]'));
$title .= $description;
$title .= html_writer::tag('label', get_string('descriptionmarkers', 'gradingform_guide'),
array('for'=>'{NAME}[criteria][{CRITERION-id}][descriptionmarkers]'));
$title .= $descriptionmarkers;
$title .= html_writer::tag('label', get_string('maxscore', 'gradingform_guide'),
array('for'=>'{NAME}[criteria][{CRITERION-id}][maxscore]'));
$title .= $maxscore;
} else if ($mode == gradingform_guide_controller::DISPLAY_PREVIEW_GRADED ||
$mode == gradingform_guide_controller::DISPLAY_VIEW) {
$title .= $description;
if (!empty($options['showmarkspercriterionstudents'])) {
$title .= html_writer::label(get_string('maxscore', 'gradingform_guide'), null);
$title .= $maxscore;
}
} else {
$title .= $description . $descriptionmarkers;
}
// Title cell params.
$titletdparams = array(
'class' => $descriptionclass,
'id' => '{NAME}-criteria-{CRITERION-id}-shortname-cell'
);
if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL &&
$mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
// Set description's cell as tab-focusable.
$titletdparams['tabindex'] = '0';
}
$criteriontemplate .= html_writer::tag('td', $title, $titletdparams);
$currentremark = '';
$currentscore = '';
if (isset($value['remark'])) {
$currentremark = $value['remark'];
}
if (isset($value['score'])) {
$currentscore = $value['score'];
}
// Element ID of the remark text area.
$remarkid = $elementname . '-criteria-' . $criterion['id'] . '-remark';
if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
$scoreclass = '';
if (!empty($validationerrors[$criterion['id']]['score'])) {
$scoreclass = 'error';
$currentscore = $validationerrors[$criterion['id']]['score']; // Show invalid score in form.
}
// Grading remark text area parameters.
$remarkparams = array(
'name' => '{NAME}[criteria][{CRITERION-id}][remark]',
'id' => $remarkid,
'cols' => '65', 'rows' => '5', 'class' => 'markingguideremark form-control',
'aria-labelledby' => '{NAME}-remarklabel{CRITERION-id}'
);
// Grading remark text area.
$input = html_writer::tag('textarea', s($currentremark), $remarkparams);
// Show the frequently-used comments chooser only if there are defined entries.
$commentchooser = '';
if (!empty($comments)) {
// Frequently used comments chooser.
$chooserbuttonid = 'criteria-' . $criterion['id'] . '-commentchooser';
$commentchooserparams = array('id' => $chooserbuttonid, 'class' => 'commentchooser btn btn-secondary');
$commentchooser = html_writer::tag('button', get_string('insertcomment', 'gradingform_guide'),
$commentchooserparams);
// Option items for the frequently used comments chooser dialog.
$commentoptions = array();
foreach ($comments as $id => $comment) {
$commentoption = new stdClass();
$commentoption->id = $id;
$commentoption->description = html_to_text(format_text($comment['description'], $comment['descriptionformat']));
$commentoptions[] = $commentoption;
}
// Include string for JS for the comment chooser title.
$this->page->requires->string_for_js('insertcomment', 'gradingform_guide');
// Include comment_chooser module.
$this->page->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
array($criterion['id'], $chooserbuttonid, $remarkid, $commentoptions));
}
// Hidden marking guide remark label.
$remarklabelparams = array(
'class' => 'hidden',
'id' => '{NAME}-remarklabel{CRITERION-id}'
);
$remarklabeltext = get_string('criterionremark', 'gradingform_guide',
format_text($criterion['shortname'], FORMAT_HTML));
$remarklabel = html_writer::label($remarklabeltext, $remarkid, false, $remarklabelparams);
$criteriontemplate .= html_writer::tag('td', $remarklabel . $input . $commentchooser, array('class' => 'remark'));
// Score input and max score.
$scoreinputparams = array(
'type' => 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][score]',
'class' => $scoreclass . ' form-control',
'id' => '{NAME}-criteria-{CRITERION-id}-score',
'size' => '3',
'value' => $currentscore,
'aria-labelledby' => '{NAME}-score-label'
);
$score = html_writer::empty_tag('input', $scoreinputparams);
$score .= html_writer::div('/' . s($criterion['maxscore']));
$criteriontemplate .= html_writer::tag('td', $score, array('class' => 'score'));
} else if ($mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) {
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
} else if ($mode == gradingform_guide_controller::DISPLAY_REVIEW ||
$mode == gradingform_guide_controller::DISPLAY_VIEW) {
// Hidden marking guide remark description.
$remarkdescparams = array(
'id' => '{NAME}-criteria-{CRITERION-id}-remark-desc'
);
$remarkdesctext = get_string('criterionremark', 'gradingform_guide', $criterion['shortname']);
$remarkdesc = html_writer::div($remarkdesctext, 'hidden', $remarkdescparams);
// Remarks cell.
$remarkdiv = html_writer::div(s($currentremark));
$remarkcellparams = array(
'class' => 'remark',
'tabindex' => '0',
'id' => '{NAME}-criteria-{CRITERION-id}-remark',
'aria-describedby' => '{NAME}-criteria-{CRITERION-id}-remark-desc'
);
$criteriontemplate .= html_writer::tag('td', $remarkdesc . $remarkdiv, $remarkcellparams);
// Score cell.
if (!empty($options['showmarkspercriterionstudents'])) {
$scorecellparams = array(
'class' => 'score',
'tabindex' => '0',
'id' => '{NAME}-criteria-{CRITERION-id}-score',
'aria-describedby' => '{NAME}-score-label'
);
$scorediv = html_writer::div(s($currentscore) . ' / ' . s($criterion['maxscore']));
$criteriontemplate .= html_writer::tag('td', $scorediv, $scorecellparams);
}
}
$criteriontemplate .= html_writer::end_tag('tr'); // Criterion.
$criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate);
$criteriontemplate = str_replace('{CRITERION-id}', $criterion['id'], $criteriontemplate);
return $criteriontemplate;
}
/**
* This function returns html code for displaying criterion. Depending on $mode it may be the
* code to edit guide, to preview the guide, to evaluate somebody or to review the evaluation.
*
* This function may be called from display_guide() to display the whole guide, or it can be
* called by itself to return a template used by JavaScript to add new empty criteria to the
* guide being designed.
* In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc.
*
* When overriding this function it is very important to remember that all elements of html
* form (in edit or evaluate mode) must have the name $elementname.
*
* Also JavaScript relies on the class names of elements and when developer changes them
* script might stop working.
*
* @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller}
* @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
* @param array $comment
* @return string
*/
public function comment_template($mode, $elementname = '{NAME}', $comment = null) {
if ($comment === null || !is_array($comment) || !array_key_exists('id', $comment)) {
$comment = array('id' => '{COMMENT-id}',
'description' => '{COMMENT-description}',
'sortorder' => '{COMMENT-sortorder}',
'class' => '{COMMENT-class}');
} else {
foreach (array('sortorder', 'description', 'class') as $key) {
// Set missing array elements to empty strings to avoid warnings.
if (!array_key_exists($key, $comment)) {
$criterion[$key] = '';
}
}
}
$commenttemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $comment['class'],
'id' => '{NAME}-comments-{COMMENT-id}'));
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
$commenttemplate .= html_writer::start_tag('td', array('class' => 'controls'));
foreach (array('moveup', 'delete', 'movedown') as $key) {
$value = get_string('comments'.$key, 'gradingform_guide');
$button = html_writer::empty_tag('input', array('type' => 'submit',
'name' => '{NAME}[comments][{COMMENT-id}]['.$key.']', 'id' => '{NAME}-comments-{COMMENT-id}-'.$key,
'value' => $value, 'title' => $value));
$commenttemplate .= html_writer::tag('div', $button, array('class' => $key));
}
$commenttemplate .= html_writer::end_tag('td'); // Controls.
$commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder']));
$description = html_writer::tag('textarea', s($comment['description']),
array('name' => '{NAME}[comments][{COMMENT-id}][description]',
'id' => '{NAME}-comments-{COMMENT-id}-description',
'aria-labelledby' => '{NAME}-comment-label', 'cols' => '65', 'rows' => '5'));
$description = html_writer::tag('div', $description, array('class'=>'criteriondesc'));
} else {
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
$commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder']));
$commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[comments][{COMMENT-id}][description]', 'value' => $comment['description']));
}
if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
$description = html_writer::tag('span', s($comment['description']),
array('name' => '{NAME}[comments][{COMMENT-id}][description]',
'title' => get_string('clicktocopy', 'gradingform_guide'),
'id' => '{NAME}[comments][{COMMENT-id}]', 'class'=>'markingguidecomment'));
} else {
$description = format_text($comment['description'], $comment['descriptionformat']);
}
// Retain newlines as <br> tags when displaying 'frequently used comments'.
$description = nl2br($description);
}
$descriptionclass = 'description';
if (isset($comment['error_description'])) {
$descriptionclass .= ' error';
}
$descriptioncellparams = array(
'class' => $descriptionclass,
'id' => '{NAME}-comments-{COMMENT-id}-description-cell'
);
// Make description cell tab-focusable when in review mode.
if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL &&
$mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
$descriptioncellparams['tabindex'] = '0';
}
$commenttemplate .= html_writer::tag('td', $description, $descriptioncellparams);
$commenttemplate .= html_writer::end_tag('tr'); // Criterion.
$commenttemplate = str_replace('{NAME}', $elementname, $commenttemplate);
$commenttemplate = str_replace('{COMMENT-id}', $comment['id'], $commenttemplate);
return $commenttemplate;
}
/**
* This function returns html code for displaying guide template (content before and after
* criteria list). Depending on $mode it may be the code to edit guide, to preview the guide,
* to evaluate somebody or to review the evaluation.
*
* This function is called from display_guide() to display the whole guide.
*
* When overriding this function it is very important to remember that all elements of html
* form (in edit or evaluate mode) must have the name $elementname.
*
* Also JavaScript relies on the class names of elements and when developer changes them
* script might stop working.
*
* @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller}
* @param array $options An array of options provided to {@link gradingform_guide_renderer::guide_edit_options()}
* @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
* @param string $criteriastr evaluated templates for this guide's criteria
* @param string $commentstr
* @return string
*/
protected function guide_template($mode, $options, $elementname, $criteriastr, $commentstr) {
$classsuffix = ''; // CSS suffix for class of the main div. Depends on the mode.
switch ($mode) {
case gradingform_guide_controller::DISPLAY_EDIT_FULL:
$classsuffix = ' editor editable';
break;
case gradingform_guide_controller::DISPLAY_EDIT_FROZEN:
$classsuffix = ' editor frozen';
break;
case gradingform_guide_controller::DISPLAY_PREVIEW:
case gradingform_guide_controller::DISPLAY_PREVIEW_GRADED:
$classsuffix = ' editor preview';
break;
case gradingform_guide_controller::DISPLAY_EVAL:
$classsuffix = ' evaluate editable';
break;
case gradingform_guide_controller::DISPLAY_EVAL_FROZEN:
$classsuffix = ' evaluate frozen';
break;
case gradingform_guide_controller::DISPLAY_REVIEW:
$classsuffix = ' review';
break;
case gradingform_guide_controller::DISPLAY_VIEW:
$classsuffix = ' view';
break;
}
$guidetemplate = html_writer::start_tag('div', array('id' => 'guide-{NAME}',
'class' => 'clearfix gradingform_guide'.$classsuffix));
// Hidden guide label.
$guidedescparams = array(
'id' => 'guide-{NAME}-desc',
'aria-hidden' => 'true'
);
$guidetemplate .= html_writer::div(get_string('guide', 'gradingform_guide'), 'hidden', $guidedescparams);
// Hidden criterion name label/description.
$guidetemplate .= html_writer::div(get_string('criterionname', 'gradingform_guide'), 'hidden',
array('id' => '{NAME}-criterion-name-label'));
// Hidden score label/description.
$guidetemplate .= html_writer::div(get_string('score', 'gradingform_guide'), 'hidden', array('id' => '{NAME}-score-label'));
// Criteria table parameters.
$criteriatableparams = array(
'class' => 'criteria',
'id' => '{NAME}-criteria',
'aria-describedby' => 'guide-{NAME}-desc');
$guidetemplate .= html_writer::tag('table', $criteriastr, $criteriatableparams);
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
$value = get_string('addcriterion', 'gradingform_guide');
$input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]',
'id' => '{NAME}-criteria-addcriterion', 'value' => $value, 'title' => $value));
$guidetemplate .= html_writer::tag('div', $input, array('class' => 'addcriterion'));
}
if (!empty($commentstr)) {
$guidetemplate .= html_writer::div(get_string('comments', 'gradingform_guide'), 'commentheader',
array('id' => '{NAME}-comments-label'));
$guidetemplate .= html_writer::div(get_string('comment', 'gradingform_guide'), 'hidden',
array('id' => '{NAME}-comment-label', 'aria-hidden' => 'true'));
$commentstableparams = array(
'class' => 'comments',
'id' => '{NAME}-comments',
'aria-describedby' => '{NAME}-comments-label');
$guidetemplate .= html_writer::tag('table', $commentstr, $commentstableparams);
}
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
$value = get_string('addcomment', 'gradingform_guide');
$input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[comments][addcomment]',
'id' => '{NAME}-comments-addcomment', 'value' => $value, 'title' => $value));
$guidetemplate .= html_writer::tag('div', $input, array('class' => 'addcomment'));
}
$guidetemplate .= $this->guide_edit_options($mode, $options);
$guidetemplate .= html_writer::end_tag('div');
return str_replace('{NAME}', $elementname, $guidetemplate);
}
/**
* Generates html template to view/edit the guide options. Expression {NAME} is used in
* template for the form element name
*
* @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller}
* @param array $options
* @return string
*/
protected function guide_edit_options($mode, $options) {
if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL
&& $mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN
&& $mode != gradingform_guide_controller::DISPLAY_PREVIEW) {
// Options are displayed only for people who can manage.
return;
}
$html = html_writer::start_tag('div', array('class' => 'options'));
$html .= html_writer::tag('div', get_string('guideoptions', 'gradingform_guide'), array('class' => 'optionsheading'));
$attrs = array('type' => 'hidden', 'name' => '{NAME}[options][optionsset]', 'value' => 1, 'class' => 'form-control');
$html .= html_writer::empty_tag('input', $attrs);
foreach ($options as $option => $value) {
$html .= html_writer::start_tag('div', array('class' => 'option '.$option));
$attrs = array('name' => '{NAME}[options]['.$option.']', 'id' => '{NAME}-options-'.$option);
switch ($option) {
case 'sortlevelsasc':
// Display option as dropdown.
$html .= html_writer::tag('span', get_string($option, 'gradingform_guide'), array('class' => 'label'));
$value = (int)(!!$value); // Make sure $value is either 0 or 1.
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
$selectoptions = array(0 => get_string($option.'0', 'gradingform_guide'),
1 => get_string($option.'1', 'gradingform_guide'));
$valuestr = html_writer::select($selectoptions, $attrs['name'], $value, false, array('id' => $attrs['id']));
$html .= html_writer::tag('span', $valuestr, array('class' => 'value'));
// TODO add here button 'Sort levels'.
} else {
$html .= html_writer::tag('span', get_string($option.$value, 'gradingform_guide'),
array('class' => 'value'));
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
$html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value));
}
}
break;
default:
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN && $value) {
$html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value));
}
// Display option as checkbox.
$attrs['type'] = 'checkbox';
$attrs['value'] = 1;
if ($value) {
$attrs['checked'] = 'checked';
}
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN ||
$mode == gradingform_guide_controller::DISPLAY_PREVIEW) {
$attrs['disabled'] = 'disabled';
unset($attrs['name']);
}
$html .= html_writer::empty_tag('input', $attrs);
$html .= html_writer::tag('label', get_string($option, 'gradingform_guide'), array('for' => $attrs['id']));
break;
}
$html .= html_writer::end_tag('div'); // Option.
}
$html .= html_writer::end_tag('div'); // Options.
return $html;
}
/**
* This function returns html code for displaying guide. Depending on $mode it may be the code
* to edit guide, to preview the guide, to evaluate somebody or to review the evaluation.
*
* It is very unlikely that this function needs to be overriden by theme. It does not produce
* any html code, it just prepares data about guide design and evaluation, adds the CSS
* class to elements and calls the functions level_template, criterion_template and
* guide_template
*
* @param array $criteria data about the guide design
* @param array $comments
* @param array $options
* @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller}
* @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
* @param array $values evaluation result
* @param array $validationerrors
* @return string
*/
public function display_guide($criteria, $comments, $options, $mode, $elementname = null, $values = null,
$validationerrors = null) {
$criteriastr = '';
$cnt = 0;
foreach ($criteria as $id => $criterion) {
$criterion['class'] = $this->get_css_class_suffix($cnt++, count($criteria) -1);
$criterion['id'] = $id;
if (isset($values['criteria'][$id])) {
$criterionvalue = $values['criteria'][$id];
} else {
$criterionvalue = null;
}
$criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $criterionvalue,
$validationerrors, $comments);
}
$cnt = 0;
$commentstr = '';
// Check if comments should be displayed.
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL ||
$mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN ||
$mode == gradingform_guide_controller::DISPLAY_PREVIEW ||
$mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) {
foreach ($comments as $id => $comment) {
$comment['id'] = $id;
$comment['class'] = $this->get_css_class_suffix($cnt++, count($comments) -1);
$commentstr .= $this->comment_template($mode, $elementname, $comment);
}
}
$output = $this->guide_template($mode, $options, $elementname, $criteriastr, $commentstr);
if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
$showdesc = get_user_preferences('gradingform_guide-showmarkerdesc', true);
$showdescstud = get_user_preferences('gradingform_guide-showstudentdesc', true);
$checked1 = array();
$checked2 = array();
$checked_s1 = array();
$checked_s2 = array();
$checked = array('checked' => 'checked');
if ($showdesc) {
$checked1 = $checked;
} else {
$checked2 = $checked;
}
if ($showdescstud) {
$checked_s1 = $checked;
} else {
$checked_s2 = $checked;
}
$radio1 = html_writer::tag('input', get_string('showmarkerdesc', 'gradingform_guide'), array('type' => 'radio',
'name' => 'showmarkerdesc',
'value' => "true")+$checked1);
$radio1 = html_writer::tag('label', $radio1);
$radio2 = html_writer::tag('input', get_string('hidemarkerdesc', 'gradingform_guide'), array('type' => 'radio',
'name' => 'showmarkerdesc',
'value' => "false")+$checked2);
$radio2 = html_writer::tag('label', $radio2);
$output .= html_writer::tag('div', $radio1 . $radio2, array('class' => 'showmarkerdesc'));
$radio1 = html_writer::tag('input', get_string('showstudentdesc', 'gradingform_guide'), array('type' => 'radio',
'name' => 'showstudentdesc',
'value' => "true")+$checked_s1);
$radio1 = html_writer::tag('label', $radio1);
$radio2 = html_writer::tag('input', get_string('hidestudentdesc', 'gradingform_guide'), array('type' => 'radio',
'name' => 'showstudentdesc',
'value' => "false")+$checked_s2);
$radio2 = html_writer::tag('label', $radio2);
$output .= html_writer::tag('div', $radio1 . $radio2, array('class' => 'showstudentdesc'));
}
return $output;
}
/**
* Help function to return CSS class names for element (first/last/even/odd) with leading space
*
* @param int $idx index of this element in the row/column
* @param int $maxidx maximum index of the element in the row/column
* @return string
*/
protected function get_css_class_suffix($idx, $maxidx) {
$class = '';
if ($idx == 0) {
$class .= ' first';
}
if ($idx == $maxidx) {
$class .= ' last';
}
if ($idx % 2) {
$class .= ' odd';
} else {
$class .= ' even';
}
return $class;
}
/**
* Displays for the student the list of instances or default content if no instances found
*
* @param array $instances array of objects of type gradingform_guide_instance
* @param string $defaultcontent default string that would be displayed without advanced grading
* @param bool $cangrade whether current user has capability to grade in this context
* @return string
*/
public function display_instances($instances, $defaultcontent, $cangrade) {
$return = '';
if (count($instances)) {
$return .= html_writer::start_tag('div', array('class' => 'advancedgrade'));
$idx = 0;
foreach ($instances as $instance) {
$return .= $this->display_instance($instance, $idx++, $cangrade);
}
$return .= html_writer::end_tag('div');
}
return $return. $defaultcontent;
}
/**
* Displays one grading instance
*
* @param gradingform_guide_instance $instance
* @param int $idx unique number of instance on page
* @param bool $cangrade whether current user has capability to grade in this context
*/
public function display_instance(gradingform_guide_instance $instance, $idx, $cangrade) {
$criteria = $instance->get_controller()->get_definition()->guide_criteria;
$options = $instance->get_controller()->get_options();
$values = $instance->get_guide_filling();
if ($cangrade) {
$mode = gradingform_guide_controller::DISPLAY_REVIEW;
} else {
$mode = gradingform_guide_controller::DISPLAY_VIEW;
}
$output = $this->box($instance->get_controller()->get_formatted_description(), 'gradingform_guide-description').
$this->display_guide($criteria, array(), $options, $mode, 'guide'.$idx, $values);
return $output;
}
/**
* Displays a confirmation message after a regrade has occured
*
* @param string $elementname
* @param int $changelevel
* @param int $value The regrade option that was used
* @return string
*/
public function display_regrade_confirmation($elementname, $changelevel, $value) {
$html = html_writer::start_tag('div', array('class' => 'gradingform_guide-regrade', 'role' => 'alert'));
if ($changelevel<=2) {
$html .= get_string('regrademessage1', 'gradingform_guide');
$selectoptions = array(
0 => get_string('regradeoption0', 'gradingform_guide'),
1 => get_string('regradeoption1', 'gradingform_guide')
);
$html .= html_writer::select($selectoptions, $elementname.'[regrade]', $value, false);
} else {
$html .= get_string('regrademessage5', 'gradingform_guide');
$html .= html_writer::empty_tag('input', array('name' => $elementname.'[regrade]', 'value' => 1, 'type' => 'hidden'));
}
$html .= html_writer::end_tag('div');
return $html;
}
/**
* Generates and returns HTML code to display information box about how guide score is converted to the grade
*
* @param array $scores
* @return string
*/
public function display_guide_mapping_explained($scores) {
$html = '';
if (!$scores) {
return $html;
}
if (isset($scores['modulegrade']) && $scores['maxscore'] != $scores['modulegrade']) {
$html .= $this->box(html_writer::tag('div', get_string('guidemappingexplained', 'gradingform_guide', (object)$scores))
, 'generalbox gradingform_guide-error');
}
return $html;
}
}
+248
View File
@@ -0,0 +1,248 @@
.gradingform_guide-regrade {
padding: 10px;
background: #fdd;
border: 1px solid #f00;
margin-bottom: 10px;
}
.gradingform_guide-restored {
padding: 10px;
background: #ffd;
border: 1px solid #ff0;
margin-bottom: 10px;
}
.gradingform_guide-error {
color: red;
font-weight: bold;
}
.gradingform_guide_editform .status {
font-weight: normal;
text-transform: uppercase;
font-size: 60%;
padding: 0.25em;
border: 1px solid #eee;
}
.gradingform_guide_editform .status.ready {
background-color: #e7f1c3;
border-color: #aea;
}
.gradingform_guide_editform .status.draft {
background-color: #f3f2aa;
border-color: #ee2;
}
.gradingform_guide.editor .criterion .controls,
.gradingform_guide .criterion .description,
.gradingform_guide .criterion .remark {
vertical-align: top;
}
.gradingform_guide.editor .criterion .controls,
.gradingform_guide.editor .criterion .description,
.gradingform_guide.editor .criterion .remark {
padding: 3px;
}
.gradingform_guide .criteria {
height: 100%;
}
.gradingform_guide .criterion {
border: 1px solid #ddd;
overflow: hidden;
}
.gradingform_guide .criterion.even {
background: #f0f0f0;
}
.gradingform_guide .criterion .description {
width: 100%;
}
.gradingform_guide .criterion .description .criterionmaxscore input {
width: 20px;
}
.gradingform_guide .criterion .description .criterionname {
font-weight: bold;
}
.gradingform_guide .criterion label {
font-weight: bold;
padding-right: 5px;
}
.gradingform_guide .plainvalue.empty {
font-style: italic;
color: #aaa;
}
.gradingform_guide .plainvalue.editname {
font-weight: bold;
}
/* Make invisible the buttons 'Move up' for the first criterion and 'Move down' for
the last, because those buttons will make no change */
.gradingform_guide.editor .criterion.first.last .controls .delete input,
.gradingform_guide.editor .criterion.first .controls .moveup input,
.gradingform_guide.editor .criterion.last .controls .movedown input {
display: none;
}
/* replace buttons with images */
.gradingform_guide.editor .delete input,
.gradingform_guide.editor .moveup input,
.gradingform_guide.editor .movedown input {
text-indent: -1000em;
cursor: pointer;
border: none;
}
.gradingform_guide.editor .criterion .controls .delete input {
width: 20px;
height: 16px;
background: transparent url([[pix:t/delete]]) no-repeat center top;
margin-top: 4px;
}
.gradingform_guide.editor .moveup input {
width: 20px;
height: 15px;
background: transparent url([[pix:t/up]]) no-repeat center top;
margin-top: 4px;
}
.gradingform_guide.editor .movedown input {
width: 20px;
height: 15px;
background: transparent url([[pix:t/down]]) no-repeat center top;
margin-top: 4px;
}
.gradingform_guide.editor .addcriterion input,
.gradingform_guide.editor .addcomment input {
background: transparent url([[pix:t/add]]) no-repeat;
display: block;
color: #555;
font-weight: bold;
text-decoration: none;
}
.gradingform_guide.editor .addcriterion input,
.gradingform_guide.editor .addcomment input {
background-position: left 5px top 8px;
height: 30px;
line-height: 29px;
margin-bottom: 14px;
padding-left: 20px;
padding-right: 10px;
}
.gradingform_guide .options .optionsheading {
font-weight: bold;
font-size: 1.1em;
padding-bottom: 5px;
}
.gradingform_guide .options .option {
padding-bottom: 2px;
}
.gradingform_guide .options .option label {
margin-left: 5px;
}
.gradingform_guide .options .option .value {
margin-left: 5px;
font-weight: bold;
}
.gradingform_guide .criterion .description.error {
background: #fdd;
}
/* special classes for elements created by guideeditor.js */
.gradingform_guide.editor .hiddenelement {
display: none;
}
.gradingform_guide.editor .pseudotablink {
background-color: transparent;
border: 0 solid;
height: 1px;
width: 1px;
color: transparent;
padding: 0;
margin: 0;
position: relative;
float: right;
}
.jsenabled .gradingform_guide .markingguidecomment {
cursor: pointer;
}
.jsenabled .gradingform_guide .markingguidecomment:before {
content: url([[pix:t/add]]);
padding-right: 2px;
}
.gradingform_guide .commentheader {
font-weight: bold;
font-size: 1.1em;
padding-bottom: 5px;
}
.jsenabled .gradingform_guide .criterionnamelabel {
display: none;
}
.jsenabled .gradingform_guide .criterionshortname {
font-weight: bold;
}
.gradingform_guide table {
width: 100%;
}
.gradingform_guide .descriptionreadonly {
vertical-align: top;
}
.gradingform_guide .criteriondescriptionmarkers {
width: 300px;
}
.gradingform_guide .markingguideremark {
margin: 0;
width: 100%;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.gradingform_guide .criteriondescriptionscore {
display: inline;
}
.gradingform_guide .score label {
display: block;
}
.gradingform_guide .score input {
margin: 0;
width: auto;
}
.gradingform_guide_comment_chooser {
max-height: 80vh;
overflow-y: auto;
}
.gradingform_guide-frequent-comments {
position: absolute;
top: 7px;
right: 0;
}
+5
View File
@@ -0,0 +1,5 @@
.gradingform_guide-fac {
position: absolute;
right: -5px;
top: 5px;
}
@@ -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/>.
}}
{{!
@template gradingform_guide/comment_chooser
Moodle comment chooser template for marking guide.
The purpose of this template is to render a list of frequently used comments that can be used by the comment chooser dialog.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* criterionId The criterion ID this chooser template is being generated for.
* comments Array of id / description pairs.
Example context (json):
{
"criterionId": "1",
"comments": [
{
"id": "1",
"description": "Test comment description 1"
},
{
"id": "2",
"description": "Test comment description 2"
}
]
}
}}
<div class="gradingform_guide_comment_chooser" id="comment_chooser">
<div class="list-group">
{{#comments}}
<button class="list-group-item list-group-item-action" id="comment-option-{{criterionId}}-{{id}}" tabindex="0">
{{description}}
</button>
{{/comments}}
</div>
</div>
@@ -0,0 +1,164 @@
{{!
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_guide/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 class="mb-3 criterion" data-gradingform-guide-role="criterion">
<div class="d-flex align-items-center">
<h5 class="description font-weight-bold mb-0">{{name}}</h5>
<button
class="btn btn-link px-1"
aria-controls="gradingform_guide-{{uniqid}}-criteria-{{id}}-description"
aria-expanded="false"
data-target="#gradingform_guide-{{uniqid}}-criteria-{{id}}-description"
data-toggle="collapse"
type="button"
>
{{# pix }} info, gradingform_guide {{/ pix }}
<span class="sr-only">{{#str}}informationforcriterion, gradingform_guide, {{name}}{{/str}}</span>
</button>
<button class="criterion-toggle btn btn-icon icon-no-margin text-reset p-0 font-weight-bold mb-0 ml-auto"
type="button"
data-toggle="collapse"
data-target="#criteria-{{id}}"
aria-expanded="true"
aria-controls="criteria-{{id}}">
<span class="collapsed-icon">
{{#pix}} t/collapsed, core {{/pix}}
<span class="sr-only">{{#str}} expandcriterion, core_grades {{/str}}</span>
</span>
<span class="expanded-icon">
{{#pix}} t/expanded, core {{/pix}}
<span class="sr-only">{{#str}} collapsecriterion, core_grades {{/str}}</span>
</span>
</button>
</div>
<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="collapse show" id="criteria-{{id}}">
<div class="mb-3">
<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"
min="0" max="{{maxscore}}"
aria-label="{{#str}}scoreforcriterion, gradingform_guide, {{name}}{{/str}}">
<span id="gradingform_guide-{{uniqid}}-help-{{id}}-score" aria-hidden="true" class="sr-only">{{!
}}{{#str}}score_help, gradingform_guide, { "criterion": {{# quote }}{{ name }}{{/ quote }}, "maxscore": {{# quote }}{{ maxscore }}{{/ quote }} }{{/str}}
</span>
</div>
<div class="mb-3 ">
<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"
aria-label="{{#str}}additionalcommentsforcriterion, gradingform_guide, {{name}}{{/str}}"
data-gradingform-guide-role="remark"
rows="2"
data-max-rows="5"
data-auto-rows
>{{remark}}</textarea>
{{#hascomments}}
<button
class="btn btn-icon form-inset-item icon-no-margin p-0 mt-1 mr-1 text-reset collapsed"
aria-controls="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark-frequent-comments"
aria-expanded="false"
data-toggle="collapse"
data-target="#gradingform_guide-{{uniqid}}-criteria-{{id}}-remark-frequent-comments"
type="button"
>
{{#pix}}plus, gradingform_guide{{/pix}}
<span class="sr-only">{{#str}}commentpickerforcriterion, gradingform_guide, {{name}}{{/str}}</span>
</button>
{{/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">
<h6>{{#str}}comments, gradingform_guide{{/str}}</h6>
<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}}
<span id="gradingform_guide-{{uniqid}}-help-{{id}}-remark" aria-hidden="true" class="sr-only">{{#str}}remark_help, gradingform_guide{{/str}}</span>
</div>
</div>
</div>
{{/criterion}}
</form>
{{#js}}
require(['gradingform_guide/grades/grader/gradingpanel/comments', 'core/auto_rows'], function(Comments, AutoRows) {
Comments.init('gradingform_guide-{{uniqid}}');
AutoRows.init(document.getElementById('gradingform_guide-{{uniqid}}'));
});
{{/js}}
@@ -0,0 +1,100 @@
@gradingform @gradingform_guide @javascript
Feature: Display marking guide information to students
In order for students to see the marking guide information
As a teacher
I should be able to change display settings for marking guide information
Background:
Given the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | name | advancedgradingmethod_submissions |
| assign | C1 | Assign 1 | guide |
And I am on the "Course 1" course page logged in as teacher1
And I go to "Assign 1" advanced grading definition page
And I set the following fields to these values:
| Name | Assign 1 marking guide |
| Description | Marking guide description |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 |
| Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 |
And I press "Save marking guide and make it ready"
Scenario: Confirm that marking guide information is not displayed after student is graded
# Update the existing marking guide to ensure that marks per criterion is displayed.
Given I click on "Edit the current form definition" "link"
And I set the field "Show marks per criterion to students" to "0"
And I press "Save"
And I am on the "Assign 1" "assign activity" page
And I go to "Student 1" "Assign 1" activity advanced grading page
And I grade by filling the marking guide with:
| Grade Criteria 1 | 50 | Excellent work! |
| Grade Criteria 2 | 20 | Try harder |
And I press "Save changes"
When I am on the "Assign 1" "assign activity" page logged in as student1
# Confirm the marking guide information display after student is graded when marking per criterion display is disabled.
# Confirm that overall grade is displayed.
Then I should see "70.00 / 100.00"
And I should see the marking guide information displayed as:
| criteria | description | remark |
| Grade Criteria 1 | Grade 1 description for students | Excellent work! |
| Grade Criteria 2 | Grade 2 description for students | Try harder |
Scenario: Confirm that marking guide information is displayed after student is graded
Given I am on the "Assign 1" "assign activity" page logged in as student1
And I should see "Grade 1 description for students" in the "Grade Criteria 1" "table_row"
And I should see "Grade 2 description for students" in the "Grade Criteria 2" "table_row"
# No grade to student1 yet.
And I should not see "70.00 / 100.00"
# No need to update marking guide as marking guide definition is already enabled by default
And I am on the "Assign 1" "assign activity" page logged in as teacher1
And I go to "Student 1" "Assign 1" activity advanced grading page
And I grade by filling the marking guide with:
| Grade Criteria 1 | 50 | Excellent work! |
| Grade Criteria 2 | 20 | Try harder |
And I press "Save changes"
When I am on the "Assign 1" "assign activity" page logged in as student1
# Student1 grade is now displayed.
Then I should see "70.00 / 100.00"
And I should see the marking guide information displayed as:
| criteria | description | remark | maxscore | criteriascore |
| Grade Criteria 1 | Grade 1 description for students | Excellent work! | 70 | 50 / 70 |
| Grade Criteria 2 | Grade 2 description for students | Try harder | 30 | 20 / 30 |
Scenario: Confirm that marking guide definition is retained when grading method is changed
Given I am on the "Assign 1" "assign activity" page
And I go to "Student 1" "Assign 1" activity advanced grading page
And I grade by filling the marking guide with:
| Grade Criteria 1 | 70 | Well done! |
| Grade Criteria 2 | 20 | Great work |
And I press "Save changes"
And I am on the "Assign 1" "assign activity editing" page
And I set the following fields to these values:
| Grading method | Simple direct grading |
And I press "Save and return to course"
When I go to "Assign 1" advanced grading page
Then I should not see "Assign 1 marking guide Ready for use"
And I should not see "Grade Critera 1"
And I should not see "Grade Critera 2"
And I am on the "Course 1" "grades > Grader report > View" page
And the following should exist in the "user-grades" table:
| -1- | -2- | -3- |
| Student 1 | student1@example.com | 90 |
And I am on the "Assign 1" "assign activity editing" page
And I set the following fields to these values:
| Grading method | Marking guide |
And I press "Save and return to course"
And I go to "Assign 1" advanced grading page
And I should see "Assign 1 marking guide Ready for use"
And I should see "Grade Criteria 1"
And I should see "Grade Criteria 2"
@@ -0,0 +1,216 @@
<?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/>.
/**
* Steps definitions for marking guides.
*
* @package gradingform_guide
* @category test
* @copyright 2015 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
use Behat\Gherkin\Node\TableNode as TableNode,
Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
Behat\Mink\Exception\ExpectationException as ExpectationException;
/**
* Steps definitions to help with marking guides.
*
* @package gradingform_guide
* @category test
* @copyright 2015 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_gradingform_guide extends behat_base {
/**
* Defines the marking guide with the provided data, following marking guide's definition grid cells.
*
* This method fills the marking guide of the marking guide definition
* form; the provided TableNode should contain one row for
* each criterion and each cell of the row should contain:
* # Criterion name, a.k.a. shortname
* # Description for students
* # Description for markers
* # Max score
*
* Works with both JS and non-JS.
*
* @When /^I define the following marking guide:$/
* @throws ExpectationException
* @param TableNode $guide
*/
public function i_define_the_following_marking_guide(TableNode $guide) {
$steptableinfo = '| Criterion name | Description for students | Description for markers | Maximum score |';
if ($criteria = $guide->getHash()) {
$addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_guide'));
foreach ($criteria as $index => $criterion) {
// Make sure the criterion array has 4 elements.
if (count($criterion) != 4) {
throw new ExpectationException(
'The criterion definition should contain name, description for students and markers, and maximum points. ' .
'Please follow this format: ' . $steptableinfo,
$this->getSession()
);
}
// On load, there's already a criterion template ready.
$shortnamevisible = false;
if ($index > 0) {
// So if the index is greater than 0, we click the Add new criterion button to add a new criterion.
$addcriterionbutton->click();
$shortnamevisible = true;
}
$criterionroot = 'guide[criteria][NEWID' . ($index + 1) . ']';
// Set the field value for the Criterion name.
$this->set_guide_field_value($criterionroot . '[shortname]', $criterion['Criterion name'], $shortnamevisible);
// Set the field value for the Description for students field.
$this->set_guide_field_value($criterionroot . '[description]', $criterion['Description for students']);
// Set the field value for the Description for markers field.
$this->set_guide_field_value($criterionroot . '[descriptionmarkers]', $criterion['Description for markers']);
// Set the field value for the Max score field.
$this->set_guide_field_value($criterionroot . '[maxscore]', $criterion['Maximum score']);
}
}
}
/**
* Defines the marking guide with the provided data, following marking guide's definition grid cells.
*
* This method fills the table of frequently used comments of the marking guide definition form.
* The provided TableNode should contain one row for each frequently used comment.
* Each row contains:
* # Comment
*
* Works with both JS and non-JS.
*
* @When /^I define the following frequently used comments:$/
* @throws ExpectationException
* @param TableNode $commentstable
*/
public function i_define_the_following_frequently_used_comments(TableNode $commentstable) {
$steptableinfo = '| Comment |';
if ($comments = $commentstable->getRows()) {
$addcommentbutton = $this->find_button(get_string('addcomment', 'gradingform_guide'));
foreach ($comments as $index => $comment) {
// Make sure the comment array has only 1 element.
if (count($comment) != 1) {
throw new ExpectationException(
'The comment cannot be empty. Please follow this format: ' . $steptableinfo,
$this->getSession()
);
}
// On load, there's already a comment template ready.
$commentfieldvisible = false;
if ($index > 0) {
// So if the index is greater than 0, we click the Add frequently used comment button to add a new criterion.
$addcommentbutton->click();
$commentfieldvisible = true;
}
$commentroot = 'guide[comments][NEWID' . ($index + 1) . ']';
// Set the field value for the frequently used comment.
$this->set_guide_field_value($commentroot . '[description]', $comment[0], $commentfieldvisible);
}
}
}
/**
* Performs grading of the student by filling out the marking guide.
* Set one line per criterion and for each criterion set "| Criterion name | Points | Remark |".
*
* @When /^I grade by filling the marking guide with:$/
*
* @throws ExpectationException
* @param TableNode $guide
* @return void
*/
public function i_grade_by_filling_the_marking_guide_with(TableNode $guide) {
$criteria = $guide->getRowsHash();
$stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' .
' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |';
// First element -> name, second -> points, third -> Remark.
foreach ($criteria as $name => $criterion) {
// We only expect the points and the remark, as the criterion name is $name.
if (count($criterion) !== 2) {
throw new ExpectationException($stepusage, $this->getSession());
}
// Numeric value here.
$points = $criterion[0];
if (!is_numeric($points)) {
throw new ExpectationException($stepusage, $this->getSession());
}
$criterionid = 0;
if ($criterionnamediv = $this->find('xpath', "//div[@class='criterionshortname'][text()='$name']")) {
$criteriondivname = $criterionnamediv->getAttribute('name');
// Criterion's name is of the format "advancedgrading[criteria][ID][shortname]".
// So just explode the string with "][" as delimiter to extract the criterion ID.
if ($nameparts = explode('][', $criteriondivname)) {
$criterionid = $nameparts[1];
}
}
if ($criterionid) {
$criterionroot = 'advancedgrading[criteria]' . '[' . $criterionid . ']';
$this->execute('behat_forms::i_set_the_field_to', array($criterionroot . '[score]', $points));
$this->execute('behat_forms::i_set_the_field_to', array($criterionroot . '[remark]', $criterion[1]));
}
}
}
/**
* Makes a hidden marking guide field visible (if necessary) and sets a value on it.
*
* @param string $name The name of the field
* @param string $value The value to set
* @param bool $visible
* @return void
*/
protected function set_guide_field_value($name, $value, $visible = false) {
// Fields are hidden by default.
if ($this->running_javascript() && $visible === false) {
$xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]";
$textnode = $this->find('xpath', $xpath);
$textnode->click();
}
// Set the value now.
$field = $this->find_field($name);
$field->setValue($value);
}
}
@@ -0,0 +1,74 @@
@gradingform @gradingform_guide
Feature: Teacher can define a marking guide
As a teacher,
I should be able to define a marking guide
Background:
Given the following "users" exist:
| username | firtname | lastname | email |
| teacher1 | Teacher | One | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | course | name | advancedgradingmethod_submissions |
| assign | C1 | Assign 1 | guide |
And I am on the "Course 1" course page logged in as teacher1
And I go to "Assign 1" advanced grading definition page
And I set the following fields to these values:
| Name | Marking guide 1 |
Scenario: No criterion added to marking guide
When I press "Save as draft"
# Confirm that criterion parameters are required
Then I should see "Criterion name can not be empty"
And I should see "Criterion max score can not be empty"
# Confirm that marking guide is not saved due to the missing criterion
And I should not see "Marking guide 1 Draft"
And I should not see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
@javascript
Scenario: Marking guide criterion is added to marking guide
Given I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Criteria 1 | Criteria 1 description for student | Criteria 1 description for marker | 70 |
| Criteria 2 | Criteria 2 description for student | Criteria 2 description for marker | 30 |
# Move Criteria 1 below Criteria 2
And I click on "Move down" "button" in the "Criteria 1" "table_row"
When I press "Save as draft"
And I go to "Assign 1" advanced grading definition page
# Confirm that the order of criterion shown matches input -- Criteria 2 is listed before Criteria 1
Then "Move down" "button" in the "Criteria 2" "table_row" should be visible
And "Move up" "button" in the "Criteria 2" "table_row" should not be visible
And "Move up" "button" in the "Criteria 1" "table_row" should be visible
And "Move down" "button" in the "Criteria 1" "table_row" should not be visible
# Confirm the other information entered were saved
And I should see "Criteria 2 description for student" in the "Criteria 2" "table_row"
And I should see "Criteria 2 description for marker" in the "Criteria 2" "table_row"
And I should see "30" in the "Criteria 2" "table_row"
And I should see "Criteria 1 description for student" in the "Criteria 1" "table_row"
And I should see "Criteria 1 description for marker" in the "Criteria 1" "table_row"
And I should see "70" in the "Criteria 1" "table_row"
Scenario: Marking guide options and frequently used comment are added to marking guide
Given I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Criteria 1 | Criteria 1 description for student | Criteria 1 description for marker | 50 |
| Criteria 2 | Criteria 2 description for student | Criteria 2 description for marker | 50 |
# Add frequently used comments and other marking guide options
And I define the following frequently used comments:
| Comment 1 |
| Comment 2 |
And I set the following fields to these values:
| Show guide definition to students | 1 |
| Show marks per criterion to students | 0 |
When I press "Save as draft"
And I go to "Assign 1" advanced grading definition page
# Confirm that frequently used comments and marking guide options specified during registration are retained
Then I should see "Comment 1"
And I should see "Comment 2"
And the field "Show guide definition to students" matches value "1"
And the field "Show marks per criterion to students" matches value "0"
@@ -0,0 +1,53 @@
@gradingform @gradingform_guide
Feature: Teacher can delete marking guide
As a teacher,
I should be able to delete a marking guide
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | One | teacher1@example.com |
| student1 | Student | One | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activities" exist:
| activity | course | name | advancedgradingmethod_submissions |
| assign | C1 | Assign 1 | guide |
And I am on the "Course 1" course page logged in as teacher1
And I go to "Assign 1" advanced grading definition page
And I set the following fields to these values:
| Name | Marking guide 1 |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Criterion 1 | Criterion 1 description for student | Criterion 1 description for markers | 100 |
And I press "Save marking guide and make it ready"
@javascript
Scenario: Delete a marking guide
Given I am on the "Assign 1" "assign activity" page
And I go to "Student One" "Assign 1" activity advanced grading page
And I grade by filling the marking guide with:
| Criterion 1 | 70 | Well done! |
And I press "Save changes"
And I go to "Assign 1" advanced grading page
When I click on "Delete the currently defined form" "link"
Then I should see "You are going to delete the grading form 'Marking guide 1' and all the associated information from 'Assign 1 (Submissions)'"
And I press "Cancel"
# Confirm that marking guide was not deleted if Cancel is pressed
And I should see "Marking guide 1 Ready for use"
And I should see "Criterion 1"
And I click on "Delete the currently defined form" "link"
And I press "Continue"
# Confirm that marking guide was deleted successfully if Continue is pressed
And I should see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
And I should not see "Marking guide 1 Ready for use"
And I should not see "Criterion 1"
And I am on the "Course 1" "grades > Grader report > View" page
And the following should exist in the "user-grades" table:
| -1- | -2- | -3- |
| Student One | student1@example.com | 70 |
@@ -0,0 +1,115 @@
@gradingform @gradingform_guide
Feature: Marking guides can be created and edited
In order to use and refine marking guide to grade students
As a teacher
I need to edit previously used marking guides
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activity" exists:
| activity | assign |
| course | C1 |
| idnumber | assign1 |
| name | Test assignment 1 name |
| intro | Test assignment description |
| section | 1 |
| assignsubmission_file_enabled | 1 |
| assignsubmission_onlinetext_enabled | 1 |
| assignsubmission_file_maxfiles | 1 |
| assignsubmission_file_maxsizebytes | 1000 |
| assignfeedback_comments_enabled | 1 |
| assignfeedback_file_enabled | 1 |
| assignfeedback_comments_commentinline | 1 |
And I am on the "Test assignment 1 name" "assign activity editing" page logged in as teacher1
And I set the following fields to these values:
| Grading method | Marking guide |
And I press "Save and return to course"
# Defining a marking guide
When I go to "Test assignment 1 name" advanced grading definition page
And I change window size to "large"
And I set the following fields to these values:
| Name | Assignment 1 marking guide |
| Description | Marking guide test description |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Guide criterion A | Guide A description for students | Guide A description for markers | 30 |
| Guide criterion B | Guide B description for students | Guide B description for markers | 30 |
| Guide criterion C | Guide C description for students | Guide C description for markers | 40 |
And I define the following frequently used comments:
| Comment 1 |
| Comment 2 |
| Comment 3 |
| Comment "4" |
And I press "Save marking guide and make it ready"
Then I should see "Ready for use"
And I should see "Guide criterion A"
And I should see "Guide criterion B"
And I should see "Guide criterion C"
And I should see "Comment 1"
And I should see "Comment 2"
And I should see "Comment 3"
And I should see "Comment \"4\""
@javascript
Scenario: Deleting criterion and comment
# Deleting criterion
When I am on "Course 1" course homepage
And I go to "Test assignment 1 name" advanced grading definition page
And I click on "Delete criterion" "button" in the "Guide criterion B" "table_row"
And I press "Yes"
And I press "Save"
Then I should see "Guide criterion A"
And I should see "Guide criterion C"
And I should see "WARNING: Your marking guide has a maximum grade of 70 points"
But I should not see "Guide criterion B"
# Deleting a frequently used comment
When I am on "Course 1" course homepage
And I go to "Test assignment 1 name" advanced grading definition page
And I click on "Delete comment" "button" in the "Comment 3" "table_row"
And I press "Yes"
And I press "Save"
Then I should see "Comment 1"
And I should see "Comment 2"
And I should see "Comment \"4\""
But I should not see "Comment 3"
@javascript
Scenario: Grading and viewing graded marking guide
# Grading a student.
When I navigate to "Assignment" in current page administration
And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
And I grade by filling the marking guide with:
| Guide criterion A | 25 | Very good |
| Guide criterion B | 20 | |
| Guide criterion C | 35 | Nice! |
# Inserting frequently used comment.
And I click on "Insert frequently used comment" "button" in the "Guide criterion B" "table_row"
And I wait "1" seconds
And I press "Comment \"4\""
And I wait "1" seconds
Then the field "Guide criterion B criterion remark" matches value "Comment \"4\""
When I press "Save changes"
And I am on the "Test assignment 1 name" "assign activity" page
And I follow "View all submissions"
# Checking that the user grade is correct.
Then I should see "80" in the "Student 1" "table_row"
And I log out
# Viewing it as a student.
And I am on the "Test assignment 1 name" "assign activity" page logged in as student1
And I should see "80" in the ".feedback" "css_element"
And I should see "Marking guide test description" in the ".feedback" "css_element"
And I should see "Very good"
And I should see "Comment \"4\""
And I should see "Nice!"
Scenario: I can use marking guides to grade and edit them later updating students grades with Javascript disabled
@@ -0,0 +1,39 @@
@gradingform @gradingform_guide
Feature: Teacher can edit a marking guide state
In order to change marking guide back to draft
As a teacher
I need to be able to edit the marking guide status
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | course | name | advancedgradingmethod_submissions |
| assign | C1 | Assign 1 | guide |
Scenario: Marking guide state can be changed to draft
Given I am on the "Course 1" course page logged in as teacher1
And I go to "Assign 1" advanced grading definition page
And I set the following fields to these values:
| Name | Assign 1 marking guide |
| Description | Marking guide description |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Grade Criteria 1 | Grade 1 description for students | Grade 1 description for markers | 70 |
| Grade Criteria 2 | Grade 2 description for students | Grade 2 description for markers | 30 |
And I press "Save marking guide and make it ready"
And I should not see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
And I should not see "Assign 1 marking guide Draft"
And I should see "Assign 1 marking guide Ready for use"
And I click on "Edit the current form definition" "link"
When I press "Save as draft"
Then I should see "Please note: the advanced grading form is not ready at the moment. Simple grading method will be used until the form has a valid status."
And I should see "Assign 1 marking guide Draft"
And I should not see "Assign 1 marking guide Ready for use"
@@ -0,0 +1,51 @@
@gradingform @gradingform_guide
Feature: Publish guide as templates
In order to save time to teachers
As a manager
I need to publish guides and make them available to all teachers
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| manager1 | Manager | 1 | manager1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "activities" exist:
| activity | course | idnumber | name | intro | advancedgradingmethod_submissions |
| assign | C1 | A1 | Test assignment 1 name | TA1 | guide |
| assign | C1 | A2 | Test assignment 2 name | TA2 | guide |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "system role assigns" exist:
| user | role | contextlevel | reference |
| manager1 | manager | System | |
And I log in as "manager1"
And I am on "Course 1" course homepage
And I go to "Test assignment 1 name" advanced grading definition page
And I set the following fields to these values:
| Name | Assignment 1 marking guide |
| Description | Marking guide test description |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum score |
| Guide criterion A | Guide A description for students | Guide A description for markers | 40 |
| Guide criterion B | Guide B description for students | Guide B description for markers | 60 |
And I define the following frequently used comments:
| Comment 1 |
And I press "Save marking guide and make it ready"
And I publish "Test assignment 1 name" grading form definition as a public template
And I log out
Scenario: Pick grading form from public template
When I log in as "teacher1"
And I am on "Course 1" course homepage
And I go to "Test assignment 2 name" advanced grading page
And I set "Test assignment 2 name" activity to use "Assignment 1 marking guide" grading form
Then I should see "Ready for use"
And I should see "Assignment 1 marking guide"
And I should see "Marking guide test description"
And I should see "Guide criterion A"
And I should see "Guide criterion B"
And I should see "Comment 1"
@@ -0,0 +1,114 @@
<?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/>.
/**
* Generator for the gradingforum_guide plugin.
*
* @package gradingform_guide
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tests\gradingform_guide\generator;
/**
* Convenience class to create guide criterion.
*
* @package gradingform_guide
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class criterion {
/** @var string $shortname A shortened name of the criterion. */
private $shortname;
/** @var string $description A description of the criterion. */
private $description;
/** @var string $descriptionmarkers A description of the criterion for markers. */
private $descriptionmarkers;
/** @var float Maximum score */
private $maxscore = 0;
/**
* Constructor for this test_criterion object
*
* @param string $shortname The shortname for the criterion
* @param string $description The description for the criterion
* @param string $descriptionmarkers The description for the marker for this criterion
* @param float $maxscore The maximum score possible for this criterion
*/
public function __construct(string $shortname, string $description, string $descriptionmarkers, float $maxscore) {
$this->shortname = $shortname;
$this->description = $description;
$this->descriptionmarkers = $descriptionmarkers;
$this->maxscore = $maxscore;
}
/**
* Get the description for this criterion.
*
* @return string
*/
public function get_description(): string {
return $this->description;
}
/**
* Get the description for markers of this criterion.
*
* @return string
*/
public function get_descriptionmarkers(): string {
return $this->descriptionmarkers;
}
/**
* Get the shortname for this criterion.
*
* @return string
*/
public function get_shortname(): string {
return $this->shortname;
}
/**
* Get the maxscore for this criterion.
*
* @return float
*/
public function get_maxscore(): float {
return $this->maxscore;
}
/**
* Get all values in an array for use when creating a new guide.
*
* @param int $sortorder
* @return array
*/
public function get_all_values(int $sortorder): array {
return [
'sortorder' => $sortorder,
'shortname' => $this->get_shortname(),
'description' => $this->get_description(),
'descriptionmarkers' => $this->get_descriptionmarkers(),
'maxscore' => $this->get_maxscore(),
];
}
}
@@ -0,0 +1,125 @@
<?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/>.
/**
* Generator for the gradingforum_guide plugin.
*
* @package gradingform_guide
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tests\gradingform_guide\generator;
use gradingform_controller;
use stdClass;
/**
* Test guide.
*
* @package gradingform_guide
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class guide {
/** @var array $criteria The criteria for this guide. */
protected $criteria = [];
/** @var string The name of this guide. */
protected $name;
/** @var string A description for this guide. */
protected $description;
/** @var array The guide options. */
protected $options = [];
/**
* Create a new gradingform_guide_generator_criterion.
*
* @param string $name
* @param string $description
*/
public function __construct(string $name, string $description) {
$this->name = $name;
$this->description = $description;
$this->set_option('alwaysshowdefinition', 1);
$this->set_option('showmarkspercriterionstudents', 1);
}
/**
* Creates the guide using the appropriate APIs.
*/
public function get_definition(): stdClass {
return (object) [
'name' => $this->name,
'description_editor' => [
'text' => $this->description,
'format' => FORMAT_HTML,
'itemid' => 1
],
'guide' => [
'criteria' => $this->get_critiera_as_array(),
'options' => $this->options,
'comments' => [],
],
'saveguide' => 'Continue',
'status' => gradingform_controller::DEFINITION_STATUS_READY,
];
}
/**
* Set an option for the rubric.
*
* @param string $key
* @param mixed $value
* @return self
*/
public function set_option(string $key, $value): self {
$this->options[$key] = $value;
return $this;
}
/**
* Adds a criterion to the guide.
*
* @param criterion $criterion The criterion object (class below).
* @return self
*/
public function add_criteria(criterion $criterion): self {
$this->criteria[] = $criterion;
return $this;
}
/**
* Get the criteria as an array for use in creation.
*
* @return array
*/
protected function get_critiera_as_array(): array {
$return = [];
foreach ($this->criteria as $index => $criterion) {
$return["NEWID{$index}"] = $criterion->get_all_values($index + 1);
}
return $return;
}
}
@@ -0,0 +1,239 @@
<?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/>.
/**
* Generator for the gradingforum_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();
require_once(__DIR__ . '/guide.php');
require_once(__DIR__ . '/criterion.php');
use tests\gradingform_guide\generator\guide;
use tests\gradingform_guide\generator\criterion;
/**
* Generator for the gradingforum_guide plugintype.
*
* @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 gradingform_guide_generator extends component_generator_base {
/**
* Create an instance of a marking guide.
*
* @param context $context
* @param string $component
* @param string $area
* @param string $name
* @param string $description
* @param array $criteria The list of criteria to add to the generated guide
* @return gradingform_guide_controller
*/
public function create_instance(
context $context,
string $component,
string $area,
string $name,
string $description,
array $criteria
): gradingform_guide_controller {
global $USER;
if ($USER->id === 0) {
throw new \coding_exception('Creation of a guide must currently be run as a user.');
}
// Fetch the controller for this context/component/area.
$generator = \testing_util::get_data_generator();
$gradinggenerator = $generator->get_plugin_generator('core_grading');
$controller = $gradinggenerator->create_instance($context, $component, $area, 'guide');
// Generate a definition for the supplied guide.
$guide = $this->get_guide($name, $description);
foreach ($criteria as $name => $options) {
$guide->add_criteria($this->get_criterion(
$name,
$options['description'],
$options['descriptionmarkers'],
$options['maxscore']
));
}
// Update the controller wih the guide definition.
$controller->update_definition($guide->get_definition());
return $controller;
}
/**
* Get a new guide for use with the guide controller.
*
* Note: This is just a helper class used to build a new definition. It does not persist the data.
*
* @param string $name
* @param string $description
* @return generator_guide
*/
protected function get_guide(string $name, string $description): guide {
return new \tests\gradingform_guide\generator\guide($name, $description);
}
/**
* Get a new criterion for use with a guide.
*
* Note: This is just a helper class used to build a new definition. It does not persist the data.
*
* @param string $shortname The shortname for the criterion
* @param string $description The description for the criterion
* @param string $descriptionmarkers The description for the marker for this criterion
* @param float $maxscore The maximum score possible for this criterion
* @return criterion
*/
protected function get_criterion(
string $shortname,
string $description,
string $descriptionmarkers,
float $maxscore
): criterion {
return new criterion($shortname, $description, $descriptionmarkers, $maxscore);
}
/**
* Given a controller instance, fetch the level and criterion information for the specified values.
*
* @param gradingform_controller $controller
* @param string $shortname The shortname to match the criterion on
* @return stdClass
*/
public function get_criterion_for_values(gradingform_controller $controller, string $shortname): ?stdClass {
$definition = $controller->get_definition();
$criteria = $definition->guide_criteria;
$criterion = array_reduce($criteria, function($carry, $criterion) use ($shortname) {
if ($criterion['shortname'] === $shortname) {
$carry = (object) $criterion;
}
return $carry;
}, null);
return $criterion;
}
/**
* Get submitted form data
*
* @param gradingform_guide_controller $controller
* @param int $itemid
* @param array $values A set of array values where the array key is the name of the criterion, and the value is an
* array with the desired score, and any remark.
*/
public function get_submitted_form_data(gradingform_guide_controller $controller, int $itemid, array $values): array {
$result = [
'itemid' => $itemid,
'criteria' => [],
];
foreach ($values as $criterionname => ['score' => $score, 'remark' => $remark]) {
$criterion = $this->get_criterion_for_values($controller, $criterionname);
$result['criteria'][$criterion->id] = [
'score' => $score,
'remark' => $remark,
];
}
return $result;
}
/**
* Generate a guide controller with sample data required for testing of this class.
*
* @param context_module $context
* @return 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, $component, $areaname, 'guide');
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
$guide = $guidegenerator->get_guide('testguide', 'Description text');
$guide->add_criteria($guidegenerator->get_criterion(
'Spelling mistakes',
'Full marks will be given for no spelling mistakes.',
'Deduct 5 points per spelling mistake made.',
25
));
$guide->add_criteria($guidegenerator->get_criterion(
'Pictures',
'Full marks will be given for including 3 pictures.',
'Give 5 points for each picture present',
15
));
$controller->update_definition($guide->get_definition());
return $controller;
}
/**
* Fetch a set of sample data.
*
* @param gradingform_guide_controller $controller
* @param int $itemid
* @param float $spellingscore
* @param string $spellingremark
* @param float $picturescore
* @param string $pictureremark
* @return array
*/
public function get_test_form_data(
gradingform_guide_controller $controller,
int $itemid,
float $spellingscore,
string $spellingremark,
float $picturescore,
string $pictureremark
): array {
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
return $guidegenerator->get_submitted_form_data($controller, $itemid, [
'Spelling mistakes' => [
'score' => $spellingscore,
'remark' => $spellingremark,
],
'Pictures' => [
'score' => $picturescore,
'remark' => $pictureremark,
],
]);
}
}
@@ -0,0 +1,282 @@
<?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/>.
/**
* Generator testcase for the gradingforum_guide generator.
*
* @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
*/
namespace gradingform_guide;
use context_module;
use gradingform_controller;
use gradingform_guide_controller;
/**
* Generator testcase for the gradingforum_guide generator.
*
* @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 generator_test extends \advanced_testcase {
/**
* Test guide creation.
*/
public function test_guide_creation(): void {
global $DB;
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
// Data for testing.
$name = 'myfirstguide';
$description = 'My first guide';
$criteria = [
'Alphabet' => [
'description' => 'How well you know your alphabet',
'descriptionmarkers' => 'Basic literacy: Alphabet',
'maxscore' => 5,
],
'Times tables' => [
'description' => 'How well you know your times-tables',
'descriptionmarkers' => 'Basic numeracy: Multiplication',
'maxscore' => 10,
],
];
// Unit under test.
$this->setUser($user);
$controller = $guidegenerator->create_instance($context, 'mod_assign', 'submission', $name, $description, $criteria);
$this->assertInstanceOf(gradingform_guide_controller::class, $controller);
$definition = $controller->get_definition();
$this->assertEquals('guide', $definition->method);
$this->assertNotEmpty($definition->id);
$this->assertEquals($name, $definition->name);
$this->assertEquals($description, $definition->description);
$this->assertEquals(gradingform_controller::DEFINITION_STATUS_READY, $definition->status);
$this->assertNotEmpty($definition->timecreated);
$this->assertNotEmpty($definition->timemodified);
$this->assertEquals($user->id, $definition->usercreated);
$this->assertNotEmpty($definition->guide_criteria);
$this->assertCount(2, $definition->guide_criteria);
// Check the alphabet criteria.
$criteriaids = array_keys($definition->guide_criteria);
$alphabet = $definition->guide_criteria[$criteriaids[0]];
$this->assertNotEmpty($alphabet['id']);
$this->assertEquals(1, $alphabet['sortorder']);
$this->assertEquals('How well you know your alphabet', $alphabet['description']);
$this->assertEquals('Basic literacy: Alphabet', $alphabet['descriptionmarkers']);
$this->assertEquals(5, $alphabet['maxscore']);
// Check the times tables criteria.
$tables = $definition->guide_criteria[$criteriaids[1]];
$this->assertNotEmpty($tables['id']);
$this->assertEquals(2, $tables['sortorder']);
$this->assertEquals('How well you know your times-tables', $tables['description']);
$this->assertEquals('Basic numeracy: Multiplication', $tables['descriptionmarkers']);
$this->assertEquals(10, $tables['maxscore']);
}
/**
* Test the get_criterion_for_values function.
* This is used for finding criterion and level information within a guide.
*/
public function test_get_criterion_for_values(): void {
global $DB;
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
// Data for testing.
$name = 'myfirstguide';
$description = 'My first guide';
$criteria = [
'Alphabet' => [
'description' => 'How well you know your alphabet',
'descriptionmarkers' => 'Basic literacy: Alphabet',
'maxscore' => 5,
],
'Times tables' => [
'description' => 'How well you know your times-tables',
'descriptionmarkers' => 'Basic numeracy: Multiplication',
'maxscore' => 10,
],
];
$this->setUser($user);
$controller = $guidegenerator->create_instance($context, 'mod_assign', 'submission', $name, $description, $criteria);
// Valid criterion.
$result = $guidegenerator->get_criterion_for_values($controller, 'Alphabet', 2);
$this->assertEquals('Alphabet', $result->shortname);
$this->assertEquals('How well you know your alphabet', $result->description);
$this->assertEquals('Basic literacy: Alphabet', $result->descriptionmarkers);
$this->assertEquals(5, $result->maxscore);
// Invalid criterion.
$result = $guidegenerator->get_criterion_for_values($controller, 'Foo', 0);
$this->assertNull($result);
}
/**
* Tests for the get_test_guide function.
*/
public function test_get_test_guide(): void {
global $DB;
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
$this->setUser($user);
$guide = $guidegenerator->get_test_guide($context, 'assign', 'submissions');
$definition = $guide->get_definition();
$this->assertEquals('testguide', $definition->name);
$this->assertEquals('Description text', $definition->description);
$this->assertEquals(gradingform_controller::DEFINITION_STATUS_READY, $definition->status);
// Should create a guide with 2 criterion.
$this->assertCount(2, $definition->guide_criteria);
}
/**
* Test the get_submitted_form_data function.
*/
public function test_get_submitted_form_data(): void {
global $DB;
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
$this->setUser($user);
$controller = $guidegenerator->get_test_guide($context, 'assign', 'submissions');
$result = $guidegenerator->get_submitted_form_data($controller, 93, [
'Spelling mistakes' => [
'score' => 10,
'remark' => 'Pretty good but you had a couple of errors',
],
'Pictures' => [
'score' => 15,
'remark' => 'Lots of nice pictures!',
]
]);
$this->assertIsArray($result);
$this->assertEquals(93, $result['itemid']);
$this->assertIsArray($result['criteria']);
$this->assertCount(2, $result['criteria']);
$spelling = $guidegenerator->get_criterion_for_values($controller, 'Spelling mistakes');
$this->assertIsArray($result['criteria'][$spelling->id]);
$this->assertEquals(10, $result['criteria'][$spelling->id]['score']);
$this->assertEquals('Pretty good but you had a couple of errors', $result['criteria'][$spelling->id]['remark']);
$pictures = $guidegenerator->get_criterion_for_values($controller, 'Pictures', 2);
$this->assertIsArray($result['criteria'][$pictures->id]);
$this->assertEquals(15, $result['criteria'][$pictures->id]['score']);
$this->assertEquals('Lots of nice pictures!', $result['criteria'][$pictures->id]['remark']);
}
/**
* Test the get_test_form_data function.
*/
public function test_get_test_form_data(): void {
global $DB;
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
$this->setUser($user);
$controller = $guidegenerator->get_test_guide($context, 'assign', 'submissions');
// Unit under test.
$result = $guidegenerator->get_test_form_data(
$controller,
1839,
10, 'Propper good speling',
0, 'ASCII art is not a picture'
);
$this->assertIsArray($result);
$this->assertEquals(1839, $result['itemid']);
$this->assertIsArray($result['criteria']);
$this->assertCount(2, $result['criteria']);
$spelling = $guidegenerator->get_criterion_for_values($controller, 'Spelling mistakes');
$this->assertIsArray($result['criteria'][$spelling->id]);
$this->assertEquals(10, $result['criteria'][$spelling->id]['score']);
$this->assertEquals('Propper good speling', $result['criteria'][$spelling->id]['remark']);
$pictures = $guidegenerator->get_criterion_for_values($controller, 'Pictures');
$this->assertIsArray($result['criteria'][$pictures->id]);
$this->assertEquals(0, $result['criteria'][$pictures->id]['score']);
$this->assertEquals('ASCII art is not a picture', $result['criteria'][$pictures->id]['remark']);
}
}
@@ -0,0 +1,398 @@
<?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/>.
declare(strict_types = 1);
namespace gradingform_guide\grades\grader\gradingpanel\external;
use advanced_testcase;
use coding_exception;
use core_grades\component_gradeitem;
use core_external\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 {
/**
* 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('warnings', $result);
$this->assertIsArray($result['warnings']);
$this->assertEmpty($result['warnings']);
// Test the grade array items.
$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('usergrade', $result['grade']);
$this->assertEquals('- / 100.00', $result['grade']['usergrade']);
$this->assertArrayHasKey('maxgrade', $result['grade']);
$this->assertIsInt($result['grade']['maxgrade']);
$this->assertEquals(100, $result['grade']['maxgrade']);
$this->assertArrayHasKey('gradedby', $result['grade']);
$this->assertEquals(null, $result['grade']['gradedby']);
$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();
[
'forum' => $forum,
'controller' => $controller,
'definition' => $definition,
'student' => $student,
'teacher' => $teacher,
] = $this->get_test_data();
$this->execute_and_assert_fetch($forum, $controller, $definition, $teacher, $teacher, $student);
}
/**
* Class mates should not get other's grades.
*/
public function test_execute_fetch_does_not_return_data_to_other_students(): void {
$this->resetAfterTest();
[
'forum' => $forum,
'controller' => $controller,
'definition' => $definition,
'student' => $student,
'teacher' => $teacher,
'course' => $course,
] = $this->get_test_data();
$evilstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->expectException(\required_capability_exception::class);
$this->execute_and_assert_fetch($forum, $controller, $definition, $evilstudent, $teacher, $student);
}
/**
* Grades can be returned to graded user.
*/
public function test_execute_fetch_return_data_to_graded_user(): void {
$this->resetAfterTest();
[
'forum' => $forum,
'controller' => $controller,
'definition' => $definition,
'student' => $student,
'teacher' => $teacher,
] = $this->get_test_data();
$this->execute_and_assert_fetch($forum, $controller, $definition, $student, $teacher, $student);
}
/**
* Executes and performs all the assertions of the fetch method with the given parameters.
*/
private function execute_and_assert_fetch($forum, $controller, $definition, $fetcheruser, $grader, $gradeduser) {
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
$this->setUser($grader);
$gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
$grade = $gradeitem->get_grade_for_user($gradeduser, $grader);
$instance = $gradeitem->get_advanced_grading_instance($grader, $grade);
$submissiondata = $guidegenerator->get_test_form_data($controller, (int) $gradeduser->id,
10, 'Propper good speling',
0, 'ASCII art is not a picture'
);
$gradeitem->store_grade_from_formdata($gradeduser, $grader, (object) [
'instanceid' => $instance->get_id(),
'advancedgrading' => $submissiondata,
]);
$this->setUser($fetcheruser);
// Set up some items we need to return on other interfaces.
$result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $gradeduser->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('warnings', $result);
$this->assertIsArray($result['warnings']);
$this->assertEmpty($result['warnings']);
// Test the grade array items.
$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('usergrade', $result['grade']);
$this->assertEquals('25.00 / 100.00', $result['grade']['usergrade']);
$this->assertArrayHasKey('maxgrade', $result['grade']);
$this->assertIsInt($result['grade']['maxgrade']);
$this->assertEquals(100, $result['grade']['maxgrade']);
$this->assertArrayHasKey('gradedby', $result['grade']);
$this->assertEquals(fullname($grader), $result['grade']['gradedby']);
$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, 'grade_forum' => 100]));
$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();
// In the situation of mod_forum this would be the id from forum_grades.
$itemid = 1;
$instance = $controller->create_instance($student->id, $itemid);
$data = $this->get_test_form_data(
$controller,
$itemid,
5, 'This user made several mistakes.',
10, 'This user has two pictures.'
);
// Update this instance with data.
$instance->update($data);
return [
'forum' => $forum,
'controller' => $controller,
'definition' => $definition,
'student' => $student,
'teacher' => $teacher,
'course' => $course,
];
}
/**
* Fetch a set of sample data.
*
* @param \gradingform_guide_controller $controller
* @param int $itemid
* @param float $spellingscore
* @param string $spellingremark
* @param float $picturescore
* @param string $pictureremark
* @return array
*/
protected function get_test_form_data(
\gradingform_guide_controller $controller,
int $itemid,
float $spellingscore,
string $spellingremark,
float $picturescore,
string $pictureremark
): array {
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
return $guidegenerator->get_test_form_data(
$controller,
$itemid,
$spellingscore,
$spellingremark,
$picturescore,
$pictureremark
);
}
}
@@ -0,0 +1,245 @@
<?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/>.
declare(strict_types = 1);
namespace gradingform_guide\grades\grader\gradingpanel\external;
use advanced_testcase;
use coding_exception;
use core_grades\component_gradeitem;
use core_external\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 {
/**
* 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, false, '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, false, '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, false, '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, false, '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, false, $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('warnings', $result);
$this->assertIsArray($result['warnings']);
$this->assertEmpty($result['warnings']);
// Test the grade array items.
$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('usergrade', $result['grade']);
$this->assertEquals('0.50 / 2.00', $result['grade']['usergrade']);
$this->assertArrayHasKey('maxgrade', $result['grade']);
$this->assertIsInt($result['grade']['maxgrade']);
$this->assertEquals(2, $result['grade']['maxgrade']);
$this->assertArrayHasKey('gradedby', $result['grade']);
$this->assertEquals(fullname($teacher), $result['grade']['gradedby']);
$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,
];
}
}
@@ -0,0 +1,94 @@
<?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/>.
namespace gradingform_guide;
use gradingform_controller;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/grade/grading/lib.php');
require_once($CFG->dirroot . '/grade/grading/form/guide/lib.php');
/**
* Test cases for the Marking Guide.
*
* @package gradingform_guide
* @category test
* @copyright 2015 Nikita Kalinin <nixorv@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class guide_test extends \advanced_testcase {
/**
* Unit test to get draft instance and create new instance.
*/
public function test_get_or_create_instance(): void {
global $DB;
$this->resetAfterTest(true);
// Create fake areas.
$fakearea = (object)array(
'contextid' => 1,
'component' => 'mod_assign',
'areaname' => 'submissions',
'activemethod' => 'guide'
);
$fakearea1id = $DB->insert_record('grading_areas', $fakearea);
$fakearea->contextid = 2;
$fakearea2id = $DB->insert_record('grading_areas', $fakearea);
// Create fake definitions.
$fakedefinition = (object)array(
'areaid' => $fakearea1id,
'method' => 'guide',
'name' => 'fakedef',
'status' => gradingform_controller::DEFINITION_STATUS_READY,
'timecreated' => 0,
'usercreated' => 1,
'timemodified' => 0,
'usermodified' => 1,
);
$fakedef1id = $DB->insert_record('grading_definitions', $fakedefinition);
$fakedefinition->areaid = $fakearea2id;
$fakedef2id = $DB->insert_record('grading_definitions', $fakedefinition);
// Create fake guide instance in first area.
$fakeinstance = (object)array(
'definitionid' => $fakedef1id,
'raterid' => 1,
'itemid' => 1,
'rawgrade' => null,
'status' => 0,
'feedback' => null,
'feedbackformat' => 0,
'timemodified' => 0
);
$fakeinstanceid = $DB->insert_record('grading_instances', $fakeinstance);
$manager1 = get_grading_manager($fakearea1id);
$manager2 = get_grading_manager($fakearea2id);
$controller1 = $manager1->get_controller('guide');
$controller2 = $manager2->get_controller('guide');
$instance1 = $controller1->get_or_create_instance(0, 1, 1);
$instance2 = $controller2->get_or_create_instance(0, 1, 1);
// Definitions should not be the same.
$this->assertEquals(false, $instance1->get_data('definitionid') == $instance2->get_data('definitionid'));
}
}
@@ -0,0 +1,217 @@
<?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/>.
/**
* Privacy tests for gradingform_guide.
*
* @package gradingform_guide
* @category test
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace gradingform_guide\privacy;
defined('MOODLE_INTERNAL') || die();
global $CFG;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\writer;
use gradingform_guide\privacy\provider;
/**
* Privacy tests for gradingform_guide.
*
* @package gradingform_guide
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/**
* Ensure that export_user_preferences returns no data if the user has no data.
*/
public function test_export_user_preferences_not_defined(): void {
$user = \core_user::get_user_by_username('admin');
provider::export_user_preferences($user->id);
$writer = writer::with_context(\context_system::instance());
$this->assertFalse($writer->has_any_data());
}
/**
* Ensure that export_user_preferences returns single preferences.
*/
public function test_export_user_preferences(): void {
$this->resetAfterTest();
// Define a user preference.
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
set_user_preference('gradingform_guide-showmarkerdesc', 0, $user);
set_user_preference('gradingform_guide-showstudentdesc', 1, $user);
// Validate exported data.
provider::export_user_preferences($user->id);
$context = \context_user::instance($user->id);
/** @var \core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context($context);
$this->assertTrue($writer->has_any_data());
$prefs = $writer->get_user_preferences('gradingform_guide');
$this->assertCount(2, (array) $prefs);
$this->assertEquals(
get_string('privacy:metadata:preference:showstudentdesc', 'gradingform_guide'),
$prefs->{'gradingform_guide-showstudentdesc'}->description
);
$this->assertEquals(get_string('no'), $prefs->{'gradingform_guide-showmarkerdesc'}->value);
$this->assertEquals(get_string('yes'), $prefs->{'gradingform_guide-showstudentdesc'}->value);
}
/**
* Test the export of guide data.
*/
public function test_get_gradingform_export_data(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$modulecontext = \context_module::instance($module->cmid);
$controller = $this->get_test_guide($modulecontext);
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
$data = $this->get_test_form_data(
$controller,
$itemid,
5, 'This user made several mistakes.',
10, 'This user has two pictures.'
);
$instance->update($data);
$instanceid = $instance->get_data('id');
// Let's try the method we are testing.
provider::export_gradingform_instance_data($modulecontext, $instance->get_id(), ['Test']);
$data = (array) writer::with_context($modulecontext)->get_data(['Test', 'Marking guide', $instanceid]);
$this->assertCount(2, $data);
$this->assertEquals('Spelling mistakes', $data['Spelling mistakes']->shortname);
$this->assertEquals('This user made several mistakes.', $data['Spelling mistakes']->remark);
$this->assertEquals('Pictures', $data['Pictures']->shortname);
$this->assertEquals('This user has two pictures.', $data['Pictures']->remark);
}
/**
* Test the deletion of guide user information via the instance ID.
*/
public function test_delete_gradingform_for_instances(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
$modulecontext = \context_module::instance($module->cmid);
$controller = $this->get_test_guide($modulecontext);
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
$data = $this->get_test_form_data(
$controller,
$itemid,
5, 'This user made several mistakes.',
10, 'This user has two pictures.'
);
$instance->update($data);
$instanceid = $instance->get_data('id');
$itemid = 2;
$instance = $controller->create_instance($user->id, $itemid);
$data = $this->get_test_form_data(
$controller,
$itemid,
25, 'This user made no mistakes.',
5, 'This user has one pictures.'
);
$instance->update($data);
$instanceid = $instance->get_data('id');
// Check how many records we have in the fillings table.
$records = $DB->get_records('gradingform_guide_fillings');
$this->assertCount(4, $records);
// Let's delete one of the instances (the last one would be the easiest).
provider::delete_gradingform_for_instances([$instance->get_id()]);
$records = $DB->get_records('gradingform_guide_fillings');
$this->assertCount(2, $records);
foreach ($records as $record) {
$this->assertNotEquals($instance->get_id(), $record->instanceid);
}
}
/**
* Generate a guide controller with sample data required for testing of this class.
*
* @param \context_module $context
* @return \gradingform_guide_controller
*/
protected function get_test_guide(\context_module $context): \gradingform_guide_controller {
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
return $guidegenerator->get_test_guide($context);
}
/**
* Fetch a set of sample data.
*
* @param \gradingform_guide_controller $controller
* @param int $itemid
* @param float $spellingscore
* @param string $spellingremark
* @param float $picturescore
* @param string $pictureremark
* @return array
*/
protected function get_test_form_data(
\gradingform_guide_controller $controller,
int $itemid,
float $spellingscore,
string $spellingremark,
float $picturescore,
string $pictureremark
): array {
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
return $guidegenerator->get_test_form_data(
$controller,
$itemid,
$spellingscore,
$spellingremark,
$picturescore,
$pictureremark
);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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/>.
/**
* Marking guide, advanced grade plugin
*
* @package gradingform_guide
* @copyright 2012 Dan Marsden <dan@danmarsden.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'gradingform_guide';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,10 @@
define("gradingform_rubric/grades/grader/gradingpanel",["exports","core/ajax","core_grades/grades/grader/gradingpanel/normalise","core_grades/grades/grader/gradingpanel/comparison","jquery"],(function(_exports,_ajax,_normalise,_comparison,_jquery){var obj;
/**
* Grading panel for gradingform_rubric.
*
* @module gradingform_rubric/grades/grader/gradingpanel
* @copyright 2019 Mathew May <mathew.solutions>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.storeCurrentGrade=_exports.fetchCurrentGrade=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.fetchCurrentGrade=(component,contextid,itemname,gradeduserid)=>(0,_ajax.call)([{methodname:"gradingform_rubric_grader_gradingpanel_fetch",args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid}}])[0];_exports.storeCurrentGrade=async(component,contextid,itemname,gradeduserid,notifyUser,rootNode)=>{const form=rootNode.querySelector("form");return!0===(0,_comparison.compareData)(form)?(0,_normalise.normaliseResult)(await(0,_ajax.call)([{methodname:"gradingform_rubric_grader_gradingpanel_store",args:{component:component,contextid:contextid,itemname:itemname,gradeduserid:gradeduserid,notifyuser:notifyUser,formdata:(0,_jquery.default)(form).serialize()}}])[0]):""}}));
//# sourceMappingURL=gradingpanel.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"gradingpanel.min.js","sources":["../../../src/grades/grader/gradingpanel.js"],"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 <http://www.gnu.org/licenses/>.\n\n/**\n * Grading panel for gradingform_rubric.\n *\n * @module gradingform_rubric/grades/grader/gradingpanel\n * @copyright 2019 Mathew May <mathew.solutions>\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';\nimport {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';\n\n// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()\nimport jQuery from 'jquery';\n\n/**\n * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade.\n *\n * @param {String} component\n * @param {Number} contextid\n * @param {String} itemname\n * @param {Number} gradeduserid\n *\n * @returns {Promise}\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/**\n * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form.\n *\n * @param {String} component\n * @param {Number} contextid\n * @param {String} itemname\n * @param {Number} gradeduserid\n * @param {Boolean} notifyUser\n * @param {HTMLElement} rootNode\n *\n * @returns {Promise}\n */\nexport const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, notifyUser, rootNode) => {\n const form = rootNode.querySelector('form');\n\n if (compareData(form) === true) {\n return normaliseResult(await fetchMany([{\n methodname: `gradingform_rubric_grader_gradingpanel_store`,\n args: {\n component,\n contextid,\n itemname,\n gradeduserid,\n notifyuser: notifyUser,\n formdata: jQuery(form).serialize(),\n },\n }])[0]);\n } else {\n return '';\n }\n};\n"],"names":["component","contextid","itemname","gradeduserid","methodname","args","async","notifyUser","rootNode","form","querySelector","notifyuser","formdata","serialize"],"mappings":";;;;;;;6MAwCiC,CAACA,UAAWC,UAAWC,SAAUC,gBACvD,cAAU,CAAC,CACdC,0DACAC,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,iBAEJ,8BAeyBG,MAAMN,UAAWC,UAAWC,SAAUC,aAAcI,WAAYC,kBACvFC,KAAOD,SAASE,cAAc,eAEV,KAAtB,2BAAYD,OACL,oCAAsB,cAAU,CAAC,CACpCL,0DACAC,KAAM,CACFL,UAAAA,UACAC,UAAAA,UACAC,SAAAA,SACAC,aAAAA,aACAQ,WAAYJ,WACZK,UAAU,mBAAOH,MAAMI,gBAE3B,IAEG"}
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Grading panel for gradingform_rubric.
*
* @module gradingform_rubric/grades/grader/gradingpanel
* @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';
import {compareData} from 'core_grades/grades/grader/gradingpanel/comparison';
// 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 {Boolean} notifyUser
* @param {HTMLElement} rootNode
*
* @returns {Promise}
*/
export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, notifyUser, rootNode) => {
const form = rootNode.querySelector('form');
if (compareData(form) === true) {
return normaliseResult(await fetchMany([{
methodname: `gradingform_rubric_grader_gradingpanel_store`,
args: {
component,
contextid,
itemname,
gradeduserid,
notifyuser: notifyUser,
formdata: jQuery(form).serialize(),
},
}])[0]);
} else {
return '';
}
};
@@ -0,0 +1,124 @@
<?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/>.
/**
* Support for backup API
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Defines rubric backup structures
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class backup_gradingform_rubric_plugin extends backup_gradingform_plugin {
/**
* Declares rubric structures to append to the grading form definition
*/
protected function define_definition_plugin_structure() {
// Append data only if the grand-parent element has 'method' set to 'rubric'
$plugin = $this->get_plugin_element(null, '../../method', 'rubric');
// Create a visible container for our data
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
// Connect our visible container to the parent
$plugin->add_child($pluginwrapper);
// Define our elements
$criteria = new backup_nested_element('criteria');
$criterion = new backup_nested_element('criterion', array('id'), array(
'sortorder', 'description', 'descriptionformat'));
$levels = new backup_nested_element('levels');
$level = new backup_nested_element('level', array('id'), array(
'score', 'definition', 'definitionformat'));
// Build elements hierarchy
$pluginwrapper->add_child($criteria);
$criteria->add_child($criterion);
$criterion->add_child($levels);
$levels->add_child($level);
// Set sources to populate the data
$criterion->set_source_table('gradingform_rubric_criteria',
array('definitionid' => backup::VAR_PARENTID));
$level->set_source_table('gradingform_rubric_levels',
array('criterionid' => backup::VAR_PARENTID));
// no need to annotate ids or files yet (one day when criterion definition supports
// embedded files, they must be annotated here)
return $plugin;
}
/**
* Declares rubric structures to append to the grading form instances
*/
protected function define_instance_plugin_structure() {
// Append data only if the ancestor 'definition' element has 'method' set to 'rubric'
$plugin = $this->get_plugin_element(null, '../../../../method', 'rubric');
// Create a visible container for our data
$pluginwrapper = new backup_nested_element($this->get_recommended_name());
// Connect our visible container to the parent
$plugin->add_child($pluginwrapper);
// Define our elements
$fillings = new backup_nested_element('fillings');
$filling = new backup_nested_element('filling', array('id'), array(
'criterionid', 'levelid', 'remark', 'remarkformat'));
// Build elements hierarchy
$pluginwrapper->add_child($fillings);
$fillings->add_child($filling);
// Set sources to populate the data
// Binding criterionid to ensure it's existence
$filling->set_source_sql('SELECT rf.*
FROM {gradingform_rubric_fillings} rf
JOIN {grading_instances} gi ON gi.id = rf.instanceid
JOIN {gradingform_rubric_criteria} rc ON rc.id = rf.criterionid AND gi.definitionid = rc.definitionid
WHERE rf.instanceid = :instanceid',
array('instanceid' => backup::VAR_PARENTID));
// no need to annotate ids or files yet (one day when remark field supports
// embedded fileds, they must be annotated here)
return $plugin;
}
}
@@ -0,0 +1,125 @@
<?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/>.
/**
* Support for restore API
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Restores the rubric specific data from grading.xml file
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class restore_gradingform_rubric_plugin extends restore_gradingform_plugin {
/**
* Declares the rubric XML paths attached to the form definition element
*
* @return array of {@link restore_path_element}
*/
protected function define_definition_plugin_structure() {
$paths = array();
$paths[] = new restore_path_element('gradingform_rubric_criterion',
$this->get_pathfor('/criteria/criterion'));
$paths[] = new restore_path_element('gradingform_rubric_level',
$this->get_pathfor('/criteria/criterion/levels/level'));
return $paths;
}
/**
* Declares the rubric XML paths attached to the form instance element
*
* @return array of {@link restore_path_element}
*/
protected function define_instance_plugin_structure() {
$paths = array();
$paths[] = new restore_path_element('gradinform_rubric_filling',
$this->get_pathfor('/fillings/filling'));
return $paths;
}
/**
* Processes criterion element data
*
* Sets the mapping 'gradingform_rubric_criterion' to be used later by
* {@link self::process_gradinform_rubric_filling()}
*
* @param stdClass|array $data
*/
public function process_gradingform_rubric_criterion($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->definitionid = $this->get_new_parentid('grading_definition');
$newid = $DB->insert_record('gradingform_rubric_criteria', $data);
$this->set_mapping('gradingform_rubric_criterion', $oldid, $newid);
}
/**
* Processes level element data
*
* Sets the mapping 'gradingform_rubric_level' to be used later by
* {@link self::process_gradinform_rubric_filling()}
*
* @param stdClass|array $data
*/
public function process_gradingform_rubric_level($data) {
global $DB;
$data = (object)$data;
$oldid = $data->id;
$data->criterionid = $this->get_new_parentid('gradingform_rubric_criterion');
$newid = $DB->insert_record('gradingform_rubric_levels', $data);
$this->set_mapping('gradingform_rubric_level', $oldid, $newid);
}
/**
* Processes filling element data
*
* @param stdClass|array $data
*/
public function process_gradinform_rubric_filling($data) {
global $DB;
$data = (object)$data;
$data->instanceid = $this->get_new_parentid('grading_instance');
$data->criterionid = $this->get_mappingid('gradingform_rubric_criterion', $data->criterionid);
$data->levelid = $this->get_mappingid('gradingform_rubric_level', $data->levelid);
if (!empty($data->criterionid)) {
$DB->insert_record('gradingform_rubric_fillings', $data);
}
}
}
@@ -0,0 +1,330 @@
<?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 core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\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 {
global $CFG, $USER;
require_once("{$CFG->libdir}/gradelib.php");
[
'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, '*', MUST_EXIST);
// One can access its own grades. Others just if they're graders.
if ($gradeduserid != $USER->id) {
$gradeitem->require_user_can_grade($gradeduser, $USER);
}
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.
$hasgrade = $gradeitem->user_has_grade($gradeduser);
$grade = $gradeitem->get_formatted_grade_for_user($gradeduser, $USER);
$instance = $gradeitem->get_advanced_grading_instance($USER, $grade);
if (!$instance) {
throw new moodle_exception('error:gradingunavailable', 'grading');
}
$controller = $instance->get_controller();
$definition = $controller->get_definition();
$fillings = $instance->get_rubric_filling();
$context = $controller->get_context();
$definitionid = (int) $definition->id;
// Set up some items we need to return on other interfaces.
$gradegrade = \grade_grade::fetch(['itemid' => $gradeitem->get_grade_item()->id, 'userid' => $gradeduser->id]);
$gradername = $gradegrade ? fullname(\core_user::get_user($gradegrade->usermodified)) : null;
$maxgrade = max(array_keys($controller->get_grade_range()));
$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, $hasgrade) {
// 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' => get_string('notset', 'gradingform_rubric'),
'checked' => !$hasgrade,
];
// 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',
'hasgrade' => $hasgrade,
'grade' => [
'instanceid' => $instance->get_id(),
'criteria' => $criterion,
'rubricmode' => 'evaluate editable',
'teacherdescription' => $teacherdescription,
'canedit' => false,
'usergrade' => $grade->usergrade,
'maxgrade' => $maxgrade,
'gradedby' => $gradername,
'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'),
'hasgrade' => new external_value(PARAM_BOOL, 'Does the user have a grade?'),
'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'),
'usergrade' => new external_value(PARAM_RAW, 'Current user grade'),
'maxgrade' => new external_value(PARAM_RAW, 'Max possible grade'),
'gradedby' => new external_value(PARAM_RAW, 'The assumed grader of this grading instance'),
'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] = \core_external\util::format_text(
$text,
$format,
$context,
'grading',
$filearea,
$definitionid,
$formatoptions
);
return $newtext;
}
}
@@ -0,0 +1,184 @@
<?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 core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\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
),
'notifyuser' => new external_value(
PARAM_BOOL,
'Wheteher to notify the user or not',
VALUE_DEFAULT,
false
),
'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
* @param bool $notifyuser
* @return array
* @throws coding_exception
* @throws moodle_exception
* @since Moodle 3.8
*/
public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid, bool $notifyuser,
string $formdata): array {
global $USER;
[
'component' => $component,
'contextid' => $contextid,
'itemname' => $itemname,
'gradeduserid' => $gradeduserid,
'notifyuser' => $notifyuser,
'formdata' => $formdata,
] = self::validate_parameters(self::execute_parameters(), [
'component' => $component,
'contextid' => $contextid,
'itemname' => $itemname,
'gradeduserid' => $gradeduserid,
'notifyuser' => $notifyuser,
'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);
// Notify.
if ($notifyuser) {
// Send notification.
$gradeitem->send_student_notification($gradeduser, $USER);
}
// 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();
}
}
@@ -0,0 +1,89 @@
<?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/>.
/**
* Privacy class for requesting user data.
*
* @package gradingform_rubric
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace gradingform_rubric\privacy;
defined('MOODLE_INTERNAL') || die();
use \core_privacy\local\metadata\collection;
/**
* Privacy class for requesting user data.
*
* @copyright 2018 Sara Arjona <sara@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_grading\privacy\gradingform_provider_v2 {
/**
* Returns meta data about this system.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table('gradingform_rubric_fillings', [
'instanceid' => 'privacy:metadata:instanceid',
'criterionid' => 'privacy:metadata:criterionid',
'levelid' => 'privacy:metadata:levelid',
'remark' => 'privacy:metadata:remark'
], 'privacy:metadata:fillingssummary');
return $collection;
}
/**
* Export user data relating to an instance ID.
*
* @param \context $context Context to use with the export writer.
* @param int $instanceid The instance ID to export data for.
* @param array $subcontext The directory to export this data to.
*/
public static function export_gradingform_instance_data(\context $context, int $instanceid, array $subcontext) {
global $DB;
// Get records from the provided params.
$params = ['instanceid' => $instanceid];
$sql = "SELECT rc.description, rl.definition, rl.score, rf.remark
FROM {gradingform_rubric_fillings} rf
JOIN {gradingform_rubric_criteria} rc ON rc.id = rf.criterionid
JOIN {gradingform_rubric_levels} rl ON rf.levelid = rl.id
WHERE rf.instanceid = :instanceid";
$records = $DB->get_records_sql($sql, $params);
if ($records) {
$subcontext = array_merge($subcontext, [get_string('rubric', 'gradingform_rubric'), $instanceid]);
\core_privacy\local\request\writer::with_context($context)->export_data($subcontext, (object) $records);
}
}
/**
* Deletes all user data related to the provided instance IDs.
*
* @param array $instanceids The instance IDs to delete information from.
*/
public static function delete_gradingform_for_instances(array $instanceids) {
global $DB;
$DB->delete_records_list('gradingform_rubric_fillings', 'instanceid', $instanceids);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="grade/grading/form/rubric/db" VERSION="20120122" COMMENT="XMLDB file for Moodle rubrics"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="gradingform_rubric_criteria" COMMENT="Stores the rows of the rubric grid.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="definitionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the form definition this criterion is part of"/>
<FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Defines the order of the criterion in the rubric"/>
<FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The criterion description"/>
<FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the description field"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_definitionid" TYPE="foreign" FIELDS="definitionid" REFTABLE="grading_definitions" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="gradingform_rubric_levels" COMMENT="Stores the columns of the rubric grid.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="criterionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The rubric criterion we are level of"/>
<FIELD NAME="score" TYPE="number" LENGTH="10" NOTNULL="true" SEQUENCE="false" DECIMALS="5" COMMENT="The score for this level"/>
<FIELD NAME="definition" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The optional text describing the level"/>
<FIELD NAME="definitionformat" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the definition field"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_criterionid" TYPE="foreign" FIELDS="criterionid" REFTABLE="gradingform_rubric_criteria" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="gradingform_rubric_fillings" COMMENT="Stores the data of how the rubric is filled by a particular rater">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="instanceid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the grading form instance"/>
<FIELD NAME="criterionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the criterion (row) in the rubric"/>
<FIELD NAME="levelid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="If a particular level was selected during the assessment, its ID is stored here"/>
<FIELD NAME="remark" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Side note feedback regarding this particular criterion"/>
<FIELD NAME="remarkformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the remark field"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="fk_instanceid" TYPE="foreign" FIELDS="instanceid" REFTABLE="grading_instances" REFFIELDS="id"/>
<KEY NAME="fk_criterionid" TYPE="foreign" FIELDS="criterionid" REFTABLE="gradingform_rubric_criteria" REFFIELDS="id"/>
<KEY NAME="uq_instance_criterion" TYPE="unique" FIELDS="instanceid, criterionid"/>
</KEYS>
<INDEXES>
<INDEX NAME="ix_levelid" UNIQUE="false" FIELDS="levelid" COMMENT="levelid acts as a foreign key but null values are allowed"/>
</INDEXES>
</TABLE>
</TABLES>
</XMLDB>
+43
View File
@@ -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/>.
/**
* 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',
'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',
'description' => 'Store the grading data for a user from the grader grading panel.',
'type' => 'write',
'ajax' => true,
],
];
+45
View File
@@ -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/>.
/**
* This file keeps track of upgrades to plugin gradingform_rubric
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* Keeps track or rubric plugin upgrade path
*
* @param int $oldversion the DB version of currently installed plugin
* @return bool true
*/
function xmldb_gradingform_rubric_upgrade($oldversion) {
// Automatically generated Moodle v4.1.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.2.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.3.0 release upgrade line.
// Put any upgrade step following this.
// Automatically generated Moodle v4.4.0 release upgrade line.
// Put any upgrade step following this.
return true;
}
+74
View File
@@ -0,0 +1,74 @@
<?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 editor page
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__.'/../../../../config.php');
require_once(__DIR__.'/lib.php');
require_once(__DIR__.'/edit_form.php');
require_once($CFG->dirroot.'/grade/grading/lib.php');
$areaid = required_param('areaid', PARAM_INT);
$manager = get_grading_manager($areaid);
list($context, $course, $cm) = get_context_info_array($manager->get_context()->id);
require_login($course, true, $cm);
require_capability('moodle/grade:managegradingforms', $context);
$controller = $manager->get_controller('rubric');
$PAGE->set_url(new moodle_url('/grade/grading/form/rubric/edit.php', array('areaid' => $areaid)));
$PAGE->set_title(get_string('definerubric', 'gradingform_rubric'));
$PAGE->set_heading(get_string('definerubric', 'gradingform_rubric'));
$mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'context' => $context, 'allowdraft' => !$controller->has_active_instances()), 'post', '', array('class' => 'gradingform_rubric_editform'));
$data = $controller->get_definition_for_editing(true);
$returnurl = optional_param('returnurl', $manager->get_management_url(), PARAM_LOCALURL);
$data->returnurl = $returnurl;
$mform->set_data($data);
if ($mform->is_cancelled()) {
redirect($returnurl);
} else if ($mform->is_submitted() && $mform->is_validated() && !$mform->need_confirm_regrading($controller)) {
// Everything ok, validated, re-grading confirmed if needed. Make changes to the rubric.
$data = $mform->get_data();
$controller->update_definition($data);
// If we do not go back to management url and the minscore warning needs to be displayed, display it during redirection.
$warning = null;
if (!empty($data->returnurl) && $data->returnurl !== $manager->get_management_url()->out(false)) {
if (empty($data->rubric['options']['lockzeropoints']) && ($scores = $controller->get_min_max_score()) && $scores['minscore'] <> 0) {
$warning = get_string('zerolevelsabsent', 'gradingform_rubric').'<br>'.
html_writer::link($manager->get_management_url(), get_string('back'));
}
}
redirect($returnurl, $warning, null, \core\output\notification::NOTIFY_ERROR);
}
// Try to keep the session alive on this page as it may take some time
// before significant interaction happens with the server.
\core\session\manager::keepalive();
echo $OUTPUT->header();
$mform->display();
echo $OUTPUT->footer();
+217
View File
@@ -0,0 +1,217 @@
<?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/>.
/**
* The form used at the rubric editor page is defined here
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy <marina@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/lib/formslib.php');
require_once(__DIR__.'/rubriceditor.php');
MoodleQuickForm::registerElementType('rubriceditor', $CFG->dirroot.'/grade/grading/form/rubric/rubriceditor.php', 'MoodleQuickForm_rubriceditor');
/**
* Defines the rubric edit form
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy <marina@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class gradingform_rubric_editrubric extends moodleform {
/**
* Form element definition
*/
public function definition() {
$form = $this->_form;
$form->addElement('hidden', 'areaid');
$form->setType('areaid', PARAM_INT);
$form->addElement('hidden', 'returnurl');
$form->setType('returnurl', PARAM_LOCALURL);
// name
$form->addElement('text', 'name', get_string('name', 'gradingform_rubric'), array('size' => 52, 'aria-required' => 'true'));
$form->addRule('name', get_string('required'), 'required', null, 'client');
$form->setType('name', PARAM_TEXT);
// description
$options = gradingform_rubric_controller::description_form_field_options($this->_customdata['context']);
$form->addElement('editor', 'description_editor', get_string('description', 'gradingform_rubric'), null, $options);
$form->setType('description_editor', PARAM_RAW);
// rubric completion status
$choices = array();
$choices[gradingform_controller::DEFINITION_STATUS_DRAFT] = html_writer::tag('span', get_string('statusdraft', 'core_grading'), array('class' => 'status draft'));
$choices[gradingform_controller::DEFINITION_STATUS_READY] = html_writer::tag('span', get_string('statusready', 'core_grading'), array('class' => 'status ready'));
$form->addElement('select', 'status', get_string('rubricstatus', 'gradingform_rubric'), $choices)->freeze();
// rubric editor
$form->addElement('rubriceditor', 'rubric', get_string('rubric', 'gradingform_rubric'));
$form->setType('rubric', PARAM_RAW);
$buttonarray = array();
$buttonarray[] = &$form->createElement('submit', 'saverubric', get_string('saverubric', 'gradingform_rubric'));
if ($this->_customdata['allowdraft']) {
$buttonarray[] = &$form->createElement('submit', 'saverubricdraft', get_string('saverubricdraft', 'gradingform_rubric'));
}
$editbutton = &$form->createElement('submit', 'editrubric', ' ');
$editbutton->freeze();
$buttonarray[] = &$editbutton;
$buttonarray[] = &$form->createElement('cancel');
$form->addGroup($buttonarray, 'buttonar', '', array(' '), false);
$form->closeHeaderBefore('buttonar');
}
/**
* Setup the form depending on current values. This method is called after definition(),
* data submission and set_data().
* All form setup that is dependent on form values should go in here.
*
* We remove the element status if there is no current status (i.e. rubric is only being created)
* so the users do not get confused
*/
public function definition_after_data() {
$form = $this->_form;
$el = $form->getElement('status');
if (!$el->getValue()) {
$form->removeElement('status');
} else {
$vals = array_values($el->getValue());
if ($vals[0] == gradingform_controller::DEFINITION_STATUS_READY) {
$this->findButton('saverubric')->setValue(get_string('save', 'gradingform_rubric'));
}
}
}
/**
* Form vlidation.
* If there are errors return array of errors ("fieldname"=>"error message"),
* otherwise true if ok.
*
* @param array $data array of ("fieldname"=>value) of submitted data
* @param array $files array of uploaded files "element_name"=>tmp_file_path
* @return array of "element_name"=>"error_description" if there are errors,
* or an empty array if everything is OK (true allowed for backwards compatibility too).
*/
public function validation($data, $files) {
$err = parent::validation($data, $files);
$err = array();
$form = $this->_form;
$rubricel = $form->getElement('rubric');
if ($rubricel->non_js_button_pressed($data['rubric'])) {
// if JS is disabled and button such as 'Add criterion' is pressed - prevent from submit
$err['rubricdummy'] = 1;
} else if (isset($data['editrubric'])) {
// continue editing
$err['rubricdummy'] = 1;
} else if (isset($data['saverubric']) && $data['saverubric']) {
// If user attempts to make rubric active - it needs to be validated
if ($rubricel->validate($data['rubric']) !== false) {
$err['rubricdummy'] = 1;
}
}
return $err;
}
/**
* Return submitted data if properly submitted or returns NULL if validation fails or
* if there is no submitted data.
*
* @return object submitted data; NULL if not valid or not submitted or cancelled
*/
public function get_data() {
$data = parent::get_data();
if (!empty($data->saverubric)) {
$data->status = gradingform_controller::DEFINITION_STATUS_READY;
} else if (!empty($data->saverubricdraft)) {
$data->status = gradingform_controller::DEFINITION_STATUS_DRAFT;
}
return $data;
}
/**
* Check if there are changes in the rubric and it is needed to ask user whether to
* mark the current grades for re-grading. User may confirm re-grading and continue,
* return to editing or cancel the changes
*
* @param gradingform_rubric_controller $controller
*/
public function need_confirm_regrading($controller) {
$data = $this->get_data();
if (isset($data->rubric['regrade'])) {
// we have already displayed the confirmation on the previous step
return false;
}
if (!isset($data->saverubric) || !$data->saverubric) {
// we only need confirmation when button 'Save rubric' is pressed
return false;
}
if (!$controller->has_active_instances()) {
// nothing to re-grade, confirmation not needed
return false;
}
$changelevel = $controller->update_or_check_rubric($data);
if ($changelevel == 0) {
// no changes in the rubric, no confirmation needed
return false;
}
// freeze form elements and pass the values in hidden fields
// TODO MDL-29421 description_editor does not freeze the normal way, uncomment below when fixed
$form = $this->_form;
foreach (array('rubric', 'name'/*, 'description_editor'*/) as $fieldname) {
$el =& $form->getElement($fieldname);
$el->freeze();
$el->setPersistantFreeze(true);
if ($fieldname == 'rubric') {
$el->add_regrade_confirmation($changelevel);
}
}
// replace button text 'saverubric' and unfreeze 'Back to edit' button
$this->findButton('saverubric')->setValue(get_string('continue'));
$el =& $this->findButton('editrubric');
$el->setValue(get_string('backtoediting', 'gradingform_rubric'));
$el->unfreeze();
return true;
}
/**
* Returns a form element (submit button) with the name $elementname
*
* @param string $elementname
* @return HTML_QuickForm_element
*/
protected function &findButton($elementname) {
$form = $this->_form;
$buttonar =& $form->getElement('buttonar');
$elements =& $buttonar->getElements();
foreach ($elements as $el) {
if ($el->getName() == $elementname) {
return $el;
}
}
return null;
}
}
+41
View File
@@ -0,0 +1,41 @@
M.gradingform_rubric = {};
/**
* This function is called for each rubric on page.
*/
M.gradingform_rubric.init = function(Y, options) {
Y.on('click', M.gradingform_rubric.levelclick, '#rubric-'+options.name+' .level', null, Y, options.name);
// Capture also space and enter keypress.
Y.on('key', M.gradingform_rubric.levelclick, '#rubric-' + options.name + ' .level', 'space', Y, options.name);
Y.on('key', M.gradingform_rubric.levelclick, '#rubric-' + options.name + ' .level', 'enter', Y, options.name);
Y.all('#rubric-'+options.name+' .radio').setStyle('display', 'none')
Y.all('#rubric-'+options.name+' .level').each(function (node) {
if (node.one('input[type=radio]').get('checked')) {
node.addClass('checked');
}
});
};
M.gradingform_rubric.levelclick = function(e, Y, name) {
var el = e.target
while (el && !el.hasClass('level')) el = el.get('parentNode')
if (!el) return
e.preventDefault();
el.siblings().removeClass('checked');
// Set aria-checked attribute for siblings to false.
el.siblings().setAttribute('aria-checked', 'false');
chb = el.one('input[type=radio]')
if (!chb.get('checked')) {
chb.set('checked', true)
el.addClass('checked')
// Set aria-checked attribute to true if checked.
el.setAttribute('aria-checked', 'true');
} else {
el.removeClass('checked');
// Set aria-checked attribute to false if unchecked.
el.setAttribute('aria-checked', 'false');
el.get('parentNode').all('input[type=radio]').set('checked', false)
}
}
@@ -0,0 +1,312 @@
M.gradingform_rubriceditor = {'templates' : {}, 'eventhandler' : null, 'name' : null, 'Y' : null};
/**
* This function is called for each rubriceditor on page.
*/
M.gradingform_rubriceditor.init = function(Y, options) {
M.gradingform_rubriceditor.name = options.name
M.gradingform_rubriceditor.Y = Y
M.gradingform_rubriceditor.templates[options.name] = {
'criterion' : options.criteriontemplate,
'level' : options.leveltemplate
}
M.gradingform_rubriceditor.disablealleditors()
Y.on('click', M.gradingform_rubriceditor.clickanywhere, 'body', null)
YUI().use('event-touch', function (Y) {
Y.one('body').on('touchstart', M.gradingform_rubriceditor.clickanywhere);
Y.one('body').on('touchend', M.gradingform_rubriceditor.clickanywhere);
})
M.gradingform_rubriceditor.addhandlers()
};
// Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html
M.gradingform_rubriceditor.addhandlers = function() {
var Y = M.gradingform_rubriceditor.Y
var name = M.gradingform_rubriceditor.name
if (M.gradingform_rubriceditor.eventhandler) M.gradingform_rubriceditor.eventhandler.detach()
M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.buttonclick, '#rubric-'+name+' input[type=submit]', null);
}
// switches all input text elements to non-edit mode
M.gradingform_rubriceditor.disablealleditors = function() {
var Y = M.gradingform_rubriceditor.Y
var name = M.gradingform_rubriceditor.name
Y.all('#rubric-'+name+' .level').each( function(node) {M.gradingform_rubriceditor.editmode(node, false)} );
Y.all('#rubric-'+name+' .description').each( function(node) {M.gradingform_rubriceditor.editmode(node, false)} );
}
// function invoked on each click on the page. If level and/or criterion description is clicked
// it switches this element to edit mode. If rubric button is clicked it does nothing so the 'buttonclick'
// function is invoked
M.gradingform_rubriceditor.clickanywhere = function(e) {
if (e.type == 'touchstart') return
var el = e.target
// if clicked on button - disablecurrenteditor, continue
if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') {
return
}
// else if clicked on level and this level is not enabled - enable it
// or if clicked on description and this description is not enabled - enable it
var focustb = false
while (el && !(el.hasClass('level') || el.hasClass('description'))) {
if (el.hasClass('score')) focustb = true
el = el.get('parentNode')
}
if (el) {
if (el.one('textarea').hasClass('hiddenelement')) {
M.gradingform_rubriceditor.disablealleditors()
M.gradingform_rubriceditor.editmode(el, true, focustb)
}
return
}
// else disablecurrenteditor
M.gradingform_rubriceditor.disablealleditors()
}
// switch the criterion description or level to edit mode or switch back
M.gradingform_rubriceditor.editmode = function(el, editmode, focustb) {
var ta = el.one('textarea')
if (!editmode && ta.hasClass('hiddenelement')) return;
if (editmode && !ta.hasClass('hiddenelement')) return;
var pseudotablink = '<span class="pseudotablink" tabindex="0"></span>',
taplain = ta.get('parentNode').one('.plainvalue'),
tbplain = null,
tb = el.one('.score input[type=text]')
// add 'plainvalue' next to textarea for description/definition and next to input text field for score (if applicable)
if (!taplain) {
ta.get('parentNode').append('<div class="plainvalue">'+pseudotablink+'<span class="textvalue">&nbsp;</span></div>')
taplain = ta.get('parentNode').one('.plainvalue')
taplain.one('.pseudotablink').on('focus', M.gradingform_rubriceditor.clickanywhere)
if (tb) {
tb.get('parentNode').append('<span class="plainvalue">'+pseudotablink+'<span class="textvalue">&nbsp;</span></span>')
tbplain = tb.get('parentNode').one('.plainvalue')
tbplain.one('.pseudotablink').on('focus', M.gradingform_rubriceditor.clickanywhere)
}
}
if (tb && !tbplain) tbplain = tb.get('parentNode').one('.plainvalue')
if (!editmode) {
// if we need to hide the input fields, copy their contents to plainvalue(s). If description/definition
// is empty, display the default text ('Click to edit ...') and add/remove 'empty' CSS class to element
var value = ta.get('value')
if (value.length) taplain.removeClass('empty')
else {
value = (el.hasClass('level')) ? M.util.get_string('levelempty', 'gradingform_rubric') : M.util.get_string('criterionempty', 'gradingform_rubric')
taplain.addClass('empty')
}
taplain.one('.textvalue').set('innerHTML', Y.Escape.html(value));
if (tb) tbplain.one('.textvalue').set('innerHTML', Y.Escape.html(tb.get('value')));
// hide/display textarea, textbox and plaintexts
taplain.removeClass('hiddenelement')
ta.addClass('hiddenelement')
if (tb) {
tbplain.removeClass('hiddenelement')
tb.addClass('hiddenelement')
}
} else {
// if we need to show the input fields, set the width/height for textarea so it fills the cell
try {
var width = parseFloat(ta.get('parentNode').getComputedStyle('width')),
height
if (el.hasClass('level')) height = parseFloat(el.getComputedStyle('height')) - parseFloat(el.one('.score').getComputedStyle('height'))
else height = parseFloat(ta.get('parentNode').getComputedStyle('height'))
ta.setStyle('width', Math.max(width-16,50)+'px')
ta.setStyle('height', Math.max(height,20)+'px')
}
catch (err) {
// this browser do not support 'computedStyle', leave the default size of the textbox
}
// hide/display textarea, textbox and plaintexts
taplain.addClass('hiddenelement')
ta.removeClass('hiddenelement')
if (tb) {
tbplain.addClass('hiddenelement')
tb.removeClass('hiddenelement')
}
}
// focus the proper input field in edit mode
if (editmode) { if (tb && focustb) tb.focus(); else ta.focus() }
}
// handler for clicking on submit buttons within rubriceditor element. Adds/deletes/rearranges criteria and/or levels on client side
M.gradingform_rubriceditor.buttonclick = function(e, confirmed) {
var Y = M.gradingform_rubriceditor.Y
var name = M.gradingform_rubriceditor.name
if (e.target.get('type') != 'submit') return;
M.gradingform_rubriceditor.disablealleditors()
var chunks = e.target.get('id').split('-'),
action = chunks[chunks.length-1]
if (chunks[0] != name || chunks[1] != 'criteria') return;
var elements_str
if (chunks.length>4 || action == 'addlevel') {
elements_str = '#rubric-'+name+' #'+name+'-criteria-'+chunks[2]+'-levels .level'
} else {
elements_str = '#rubric-'+name+' .criterion'
}
// prepare the id of the next inserted level or criterion
var newlevid = 0;
var newid = 0;
if (action == 'addcriterion' || action == 'addlevel' || action == 'duplicate' ) {
newid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .criterion');
newlevid = M.gradingform_rubriceditor.calculatenewid('#rubric-'+name+' .level');
}
if (chunks.length == 3 && action == 'addcriterion') {
// ADD NEW CRITERION
var levelsscores = [0], levidx = 1
var parentel = Y.one('#'+name+'-criteria')
if (parentel.one('>tbody')) parentel = parentel.one('>tbody')
if (parentel.all('.criterion').size()) {
var lastcriterion = parentel.all('.criterion').item(parentel.all('.criterion').size()-1).all('.level')
for (levidx=0;levidx<lastcriterion.size();levidx++) levelsscores[levidx] = lastcriterion.item(levidx).one('.score input[type=text]').get('value')
}
for (levidx;levidx<3;levidx++) levelsscores[levidx] = parseFloat(levelsscores[levidx-1])+1
var levelsstr = '';
for (levidx=0;levidx<levelsscores.length;levidx++) {
levelsstr += M.gradingform_rubriceditor.templates[name].level.
replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx)).
replace(/\{LEVEL-score\}/g, levelsscores[levidx]).
replace(/\{LEVEL-index\}/g, levidx + 1);
}
var newcriterion = M.gradingform_rubriceditor.templates[name]['criterion'].replace(/\{LEVELS\}/, levelsstr)
parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''))
M.gradingform_rubriceditor.assignclasses('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-levels .level')
M.gradingform_rubriceditor.addhandlers();
M.gradingform_rubriceditor.disablealleditors()
M.gradingform_rubriceditor.assignclasses(elements_str)
M.gradingform_rubriceditor.editmode(
Y.one('#rubric-' + name + ' #' + name + '-criteria-NEWID' + newid + '-description-cell'), true
);
} else if (chunks.length == 5 && action == 'addlevel') {
// ADD NEW LEVEL
var newscore = 0;
parent = Y.one('#'+name+'-criteria-'+chunks[2]+'-levels')
var levelIndex = 1;
parent.all('.level').each(function (node) {
newscore = Math.max(newscore, parseFloat(node.one('.score input[type=text]').get('value')) + 1);
levelIndex++;
});
var newlevel = M.gradingform_rubriceditor.templates[name]['level'].
replace(/\{CRITERION-id\}/g, chunks[2]).replace(/\{LEVEL-id\}/g, 'NEWID'+newlevid).
replace(/\{LEVEL-score\}/g, newscore).
replace(/\{LEVEL-index\}/g, levelIndex).
replace(/\{.+?\}/g, '');
parent.append(newlevel)
M.gradingform_rubriceditor.addhandlers();
M.gradingform_rubriceditor.disablealleditors()
M.gradingform_rubriceditor.assignclasses(elements_str)
M.gradingform_rubriceditor.editmode(parent.all('.level').item(parent.all('.level').size()-1), true)
} else if (chunks.length == 4 && action == 'moveup') {
// MOVE CRITERION UP
el = Y.one('#'+name+'-criteria-'+chunks[2])
if (el.previous()) el.get('parentNode').insertBefore(el, el.previous())
M.gradingform_rubriceditor.assignclasses(elements_str)
} else if (chunks.length == 4 && action == 'movedown') {
// MOVE CRITERION DOWN
el = Y.one('#'+name+'-criteria-'+chunks[2])
if (el.next()) el.get('parentNode').insertBefore(el.next(), el)
M.gradingform_rubriceditor.assignclasses(elements_str)
} else if (chunks.length == 4 && action == 'delete') {
// DELETE CRITERION
if (confirmed) {
Y.one('#'+name+'-criteria-'+chunks[2]).remove()
M.gradingform_rubriceditor.assignclasses(elements_str)
} else {
M.util.js_pending('gradingform_rubriceditor:deleteConfirmation');
require(['core/notification', 'core/str'], function(Notification, Str) {
Notification.saveCancelPromise(
Str.get_string('confirmation', 'admin'),
Str.get_string('confirmdeletecriterion', 'gradingform_rubric'),
Str.get_string('yes', 'moodle')
).then(function() {
M.gradingform_rubriceditor.buttonclick.apply(this, [e, true]);
return;
}.bind(this)).catch(function() {
// User cancelled.
});
M.util.js_complete('gradingform_rubriceditor:deleteConfirmation');
}.bind(this));
}
} else if (chunks.length == 4 && action == 'duplicate') {
// Duplicate criterion.
var levelsdef = [], levelsscores = [0], levidx = null;
var parentel = Y.one('#'+name+'-criteria');
if (parentel.one('>tbody')) { parentel = parentel.one('>tbody'); }
var source = Y.one('#'+name+'-criteria-'+chunks[2]);
if (source.all('.level')) {
var lastcriterion = source.all('.level');
for (levidx = 0; levidx < lastcriterion.size(); levidx++) {
levelsdef[levidx] = lastcriterion.item(levidx).one('.definition .textvalue').get('innerHTML');
}
for (levidx = 0; levidx < lastcriterion.size(); levidx++) {
levelsscores[levidx] = lastcriterion.item(levidx).one('.score input[type=text]').get('value');
}
}
for (levidx; levidx < 3; levidx++) { levelsscores[levidx] = parseFloat(levelsscores[levidx-1]) + 1; }
var levelsstr = '';
for (levidx = 0; levidx < levelsscores.length; levidx++) {
levelsstr += M.gradingform_rubriceditor.templates[name].level
.replace(/\{LEVEL-id\}/g, 'NEWID'+(newlevid+levidx))
.replace(/\{LEVEL-score\}/g, levelsscores[levidx])
.replace(/\{LEVEL-definition\}/g, levelsdef[levidx]);
}
var description = source.one('.description .textvalue');
var newcriterion = M.gradingform_rubriceditor.templates[name].criterion
.replace(/\{LEVELS\}/, levelsstr)
.replace(/\{CRITERION-description\}/, description.get('innerHTML'));
parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''));
M.gradingform_rubriceditor.assignclasses('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-levels .level');
M.gradingform_rubriceditor.addhandlers();
M.gradingform_rubriceditor.disablealleditors();
M.gradingform_rubriceditor.assignclasses(elements_str);
M.gradingform_rubriceditor.editmode(Y.one('#rubric-'+name+' #'+name+'-criteria-NEWID'+newid+'-description-cell'),true);
} else if (chunks.length == 6 && action == 'delete') {
// DELETE LEVEL
if (confirmed) {
Y.one('#'+name+'-criteria-'+chunks[2]+'-'+chunks[3]+'-'+chunks[4]).remove()
M.gradingform_rubriceditor.assignclasses(elements_str)
} else {
M.util.js_pending('gradingform_rubriceditor:deleteLevelConfirmation');
require(['core/notification', 'core/str'], function(Notification, Str) {
Notification.saveCancelPromise(
Str.get_string('confirmation', 'admin'),
Str.get_string('confirmdeletelevel', 'gradingform_rubric'),
Str.get_string('yes', 'moodle')
).then(function() {
M.gradingform_rubriceditor.buttonclick.apply(this, [e, true]);
return;
}.bind(this)).catch(function() {
// User cancelled.
});
M.util.js_complete('gradingform_rubriceditor:deleteLevelConfirmation');
}.bind(this));
}
} else {
// unknown action
return;
}
e.preventDefault();
}
// properly set classes (first/last/odd/even), level width and/or criterion sortorder for elements Y.all(elements_str)
M.gradingform_rubriceditor.assignclasses = function (elements_str) {
var elements = M.gradingform_rubriceditor.Y.all(elements_str)
for (var i=0;i<elements.size();i++) {
elements.item(i).removeClass('first').removeClass('last').removeClass('even').removeClass('odd').
addClass(((i%2)?'odd':'even') + ((i==0)?' first':'') + ((i==elements.size()-1)?' last':''))
elements.item(i).all('input[type=hidden]').each(
function(node) {if (node.get('name').match(/sortorder/)) node.set('value', i)}
);
if (elements.item(i).hasClass('level')) elements.item(i).set('width', Math.round(100/elements.size())+'%')
}
}
// returns unique id for the next added element, it should not be equal to any of Y.all(elements_str) ids
M.gradingform_rubriceditor.calculatenewid = function (elements_str) {
var newid = 1
M.gradingform_rubriceditor.Y.all(elements_str).each( function(node) {
var idchunks = node.get('id').split('-'), id = idchunks.pop();
if (id.match(/^NEWID(\d+)$/)) newid = Math.max(newid, parseInt(id.substring(5))+1);
} );
return newid
}
@@ -0,0 +1,99 @@
<?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/>.
/**
* Language file for plugin gradingform_rubric
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$string['addcriterion'] = 'Add criterion';
$string['additionalfeedback'] = 'Additional feedback';
$string['alwaysshowdefinition'] = 'Allow users to preview rubric (otherwise it will only be displayed after grading)';
$string['backtoediting'] = 'Back to editing';
$string['confirmdeletecriterion'] = 'Are you sure you want to delete this criterion?';
$string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
$string['criterion'] = 'Criterion {$a}';
$string['criterionaddlevel'] = 'Add level';
$string['criteriondelete'] = 'Delete criterion';
$string['criterionduplicate'] = 'Duplicate criterion';
$string['criterionempty'] = 'Click to edit criterion';
$string['criterionmovedown'] = 'Move down';
$string['criterionmoveup'] = 'Move up';
$string['criterionremark'] = 'Remark for criterion {$a->description}: {$a->remark}';
$string['definerubric'] = 'Define rubric';
$string['description'] = 'Description';
$string['enableremarks'] = 'Allow grader to add text remarks for each criterion';
$string['err_mintwolevels'] = 'Each criterion must have at least two levels';
$string['err_nocriteria'] = 'Rubric must contain at least one criterion';
$string['err_nodefinition'] = 'Level definition can not be empty';
$string['err_nodescription'] = 'Criterion description can not be empty';
$string['err_novariations'] = 'Criterion levels cannot all be worth the same number of points';
$string['err_scoreformat'] = 'Number of points for each level must be a valid number';
$string['err_totalscore'] = 'Maximum number of points possible when graded by the rubric must be more than zero';
$string['gradingof'] = '{$a} grading';
$string['level'] = 'Level {$a->definition}, {$a->score} points.';
$string['leveldelete'] = 'Delete level {$a}';
$string['leveldefinition'] = 'Level {$a} definition';
$string['levelempty'] = 'Click to edit level';
$string['levelsgroup'] = 'Levels group';
$string['lockzeropoints'] = 'Calculate grade having a minimum score of the minimum achievable grade for the rubric';
$string['lockzeropoints_help'] = 'This setting only applies if the sum of the minimum number of points for each criterion is greater than 0. If ticked, the minimum score of the activity will be the minimum achievable grade for the rubric. If unticked, the minimum possible score for the rubric will be mapped to the minimum grade available for the activity (which is 0 unless a scale is used).';
$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['notset'] = 'Not set';
$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.';
$string['privacy:metadata:instanceid'] = 'An identifier relating to a grade in an activity.';
$string['privacy:metadata:levelid'] = 'The level obtained in the rubric.';
$string['privacy:metadata:remark'] = 'Remarks related to the rubric criterion being assessed.';
$string['regrademessage1'] = 'You are about to save changes to a rubric that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the rubric will be hidden from students until their item is regraded.';
$string['regrademessage5'] = 'You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded.';
$string['regradeoption0'] = 'Do not mark for regrade';
$string['regradeoption1'] = 'Mark for regrade';
$string['restoredfromdraft'] = 'NOTE: The last attempt to grade this person was not saved properly so draft grades have been restored. If you want to cancel these changes use the \'Cancel\' button below.';
$string['rubric'] = 'Rubric';
$string['rubricmapping'] = 'Score to grade mapping rules';
$string['rubricmappingexplained'] = 'The minimum possible score for this rubric is <b>{$a->minscore} points</b>. It will be converted to the minimum grade available for the activity (which is 0 unless a scale is used). The maximum score of <b>{$a->maxscore} points</b> will be converted to the maximum grade. Intermediate scores will be converted respectively.
If a scale is used for grading, the score will be rounded and converted to the scale elements as if they were consecutive integers.
This grade calculation may be changed by editing the form and ticking the box \'Calculate grade having a minimum score of the minimum achievable grade for the rubric\'.';
$string['rubricnotcompleted'] = 'Please choose something for each criterion';
$string['rubricoptions'] = 'Rubric options';
$string['rubricstatus'] = 'Current rubric status';
$string['save'] = 'Save';
$string['saverubric'] = 'Save rubric and make it ready';
$string['saverubricdraft'] = 'Save as draft';
$string['scoreinputforlevel'] = 'Score input for level {$a}';
$string['scorepostfix'] = '{$a} points';
$string['showdescriptionstudent'] = 'Display rubric description to those being graded';
$string['showdescriptionteacher'] = 'Display rubric description during evaluation';
$string['showremarksstudent'] = 'Show remarks to those being graded';
$string['showscorestudent'] = 'Display points for each level to those being graded';
$string['showscoreteacher'] = 'Display points for each level during evaluation';
$string['sortlevelsasc'] = 'Sort order for levels:';
$string['sortlevelsasc0'] = 'Descending by number of points';
$string['sortlevelsasc1'] = 'Ascending by number of points';
$string['zerolevelsabsent'] = 'Warning: The minimum possible score for this rubric is not 0; this can result in unexpected grades for the activity. To avoid this, each criterion should have a level with 0 points.<br>
This warning may be ignored if a scale is used for grading, and the minimum levels in the rubric correspond to the minimum value of the scale.';
+990
View File
@@ -0,0 +1,990 @@
<?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/>.
/**
* Grading method controller for the Rubric plugin
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use core_external\external_format_value;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
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
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class gradingform_rubric_controller extends gradingform_controller {
// Modes of displaying the rubric (used in gradingform_rubric_renderer)
/** Rubric display mode: For editing (moderator or teacher creates a rubric) */
const DISPLAY_EDIT_FULL = 1;
/** Rubric display mode: Preview the rubric design with hidden fields */
const DISPLAY_EDIT_FROZEN = 2;
/** Rubric display mode: Preview the rubric design (for person with manage permission) */
const DISPLAY_PREVIEW = 3;
/** Rubric display mode: Preview the rubric (for people being graded) */
const DISPLAY_PREVIEW_GRADED= 8;
/** Rubric display mode: For evaluation, enabled (teacher grades a student) */
const DISPLAY_EVAL = 4;
/** Rubric display mode: For evaluation, with hidden fields */
const DISPLAY_EVAL_FROZEN = 5;
/** Rubric display mode: Teacher reviews filled rubric */
const DISPLAY_REVIEW = 6;
/** Rubric display mode: Dispaly filled rubric (i.e. students see their grades) */
const DISPLAY_VIEW = 7;
/**
* Extends the module settings navigation with the rubric grading settings
*
* This function is called when the context for the page is an activity module with the
* FEATURE_ADVANCED_GRADING, the user has the permission moodle/grade:managegradingforms
* and there is an area with the active grading method set to 'rubric'.
*
* @param settings_navigation $settingsnav {@link settings_navigation}
* @param navigation_node $node {@link navigation_node}
*/
public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $node=null) {
$node->add(get_string('definerubric', 'gradingform_rubric'),
$this->get_editor_url(), settings_navigation::TYPE_CUSTOM,
null, null, new pix_icon('icon', '', 'gradingform_rubric'));
}
/**
* Extends the module navigation
*
* This function is called when the context for the page is an activity module with the
* FEATURE_ADVANCED_GRADING and there is an area with the active grading method set to the given plugin.
*
* @param global_navigation $navigation {@link global_navigation}
* @param navigation_node $node {@link navigation_node}
*/
public function extend_navigation(global_navigation $navigation, navigation_node $node=null) {
if (has_capability('moodle/grade:managegradingforms', $this->get_context())) {
// no need for preview if user can manage forms, he will have link to manage.php in settings instead
return;
}
if ($this->is_form_defined() && ($options = $this->get_options()) && !empty($options['alwaysshowdefinition'])) {
$node->add(get_string('gradingof', 'gradingform_rubric', get_grading_manager($this->get_areaid())->get_area_title()),
new moodle_url('/grade/grading/form/'.$this->get_method_name().'/preview.php', array('areaid' => $this->get_areaid())),
settings_navigation::TYPE_CUSTOM);
}
}
/**
* Saves the rubric definition into the database
*
* @see parent::update_definition()
* @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data()
* @param int|null $usermodified optional userid of the author of the definition, defaults to the current user
*/
public function update_definition(stdClass $newdefinition, $usermodified = null) {
$this->update_or_check_rubric($newdefinition, $usermodified, true);
if (isset($newdefinition->rubric['regrade']) && $newdefinition->rubric['regrade']) {
$this->mark_for_regrade();
}
}
/**
* Either saves the rubric definition into the database or check if it has been changed.
* Returns the level of changes:
* 0 - no changes
* 1 - only texts or criteria sortorders are changed, students probably do not require re-grading
* 2 - added levels but maximum score on rubric is the same, students still may not require re-grading
* 3 - removed criteria or added levels or changed number of points, students require re-grading but may be re-graded automatically
* 4 - removed levels - students require re-grading and not all students may be re-graded automatically
* 5 - added criteria - all students require manual re-grading
*
* @param stdClass $newdefinition rubric definition data as coming from gradingform_rubric_editrubric::get_data()
* @param int|null $usermodified optional userid of the author of the definition, defaults to the current user
* @param boolean $doupdate if true actually updates DB, otherwise performs a check
*
*/
public function update_or_check_rubric(stdClass $newdefinition, $usermodified = null, $doupdate = false) {
global $DB;
// firstly update the common definition data in the {grading_definition} table
if ($this->definition === false) {
if (!$doupdate) {
// if we create the new definition there is no such thing as re-grading anyway
return 5;
}
// if definition does not exist yet, create a blank one
// (we need id to save files embedded in description)
parent::update_definition(new stdClass(), $usermodified);
parent::load_definition();
}
if (!isset($newdefinition->rubric['options'])) {
$newdefinition->rubric['options'] = self::get_default_options();
}
$newdefinition->options = json_encode($newdefinition->rubric['options']);
$editoroptions = self::description_form_field_options($this->get_context());
$newdefinition = file_postupdate_standard_editor($newdefinition, 'description', $editoroptions, $this->get_context(),
'grading', 'description', $this->definition->id);
// reload the definition from the database
$currentdefinition = $this->get_definition(true);
$haschanges = array();
// Check if 'lockzeropoints' option has changed.
$newlockzeropoints = $newdefinition->rubric['options']['lockzeropoints'];
$currentoptions = $this->get_options();
if ((bool)$newlockzeropoints != (bool)$currentoptions['lockzeropoints']) {
$haschanges[3] = true;
}
// update rubric data
if (empty($newdefinition->rubric['criteria'])) {
$newcriteria = array();
} else {
$newcriteria = $newdefinition->rubric['criteria']; // new ones to be saved
}
$currentcriteria = $currentdefinition->rubric_criteria;
$criteriafields = array('sortorder', 'description', 'descriptionformat');
$levelfields = array('score', 'definition', 'definitionformat');
foreach ($newcriteria as $id => $criterion) {
// get list of submitted levels
$levelsdata = array();
if (array_key_exists('levels', $criterion)) {
$levelsdata = $criterion['levels'];
}
$criterionmaxscore = null;
if (preg_match('/^NEWID\d+$/', $id)) {
// insert criterion into DB
$data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE); // TODO MDL-31235 format is not supported yet
foreach ($criteriafields as $key) {
if (array_key_exists($key, $criterion)) {
$data[$key] = $criterion[$key];
}
}
if ($doupdate) {
$id = $DB->insert_record('gradingform_rubric_criteria', $data);
}
$haschanges[5] = true;
} else {
// update criterion in DB
$data = array();
foreach ($criteriafields as $key) {
if (array_key_exists($key, $criterion) && $criterion[$key] != $currentcriteria[$id][$key]) {
$data[$key] = $criterion[$key];
}
}
if (!empty($data)) {
// update only if something is changed
$data['id'] = $id;
if ($doupdate) {
$DB->update_record('gradingform_rubric_criteria', $data);
}
$haschanges[1] = true;
}
// remove deleted levels from DB and calculate the maximum score for this criteria
foreach ($currentcriteria[$id]['levels'] as $levelid => $currentlevel) {
if ($criterionmaxscore === null || $criterionmaxscore < $currentlevel['score']) {
$criterionmaxscore = $currentlevel['score'];
}
if (!array_key_exists($levelid, $levelsdata)) {
if ($doupdate) {
$DB->delete_records('gradingform_rubric_levels', array('id' => $levelid));
}
$haschanges[4] = true;
}
}
}
foreach ($levelsdata as $levelid => $level) {
if (isset($level['score'])) {
$level['score'] = unformat_float($level['score']);
}
if (preg_match('/^NEWID\d+$/', $levelid)) {
// insert level into DB
$data = array('criterionid' => $id, 'definitionformat' => FORMAT_MOODLE); // TODO MDL-31235 format is not supported yet
foreach ($levelfields as $key) {
if (array_key_exists($key, $level)) {
$data[$key] = $level[$key];
}
}
if ($doupdate) {
$levelid = $DB->insert_record('gradingform_rubric_levels', $data);
}
if ($criterionmaxscore !== null && $criterionmaxscore >= $level['score']) {
// new level is added but the maximum score for this criteria did not change, re-grading may not be necessary
$haschanges[2] = true;
} else {
$haschanges[3] = true;
}
} else {
// update level in DB
$data = array();
foreach ($levelfields as $key) {
if (array_key_exists($key, $level) && $level[$key] != $currentcriteria[$id]['levels'][$levelid][$key]) {
$data[$key] = $level[$key];
}
}
if (!empty($data)) {
// update only if something is changed
$data['id'] = $levelid;
if ($doupdate) {
$DB->update_record('gradingform_rubric_levels', $data);
}
if (isset($data['score'])) {
$haschanges[3] = true;
}
$haschanges[1] = true;
}
}
}
}
// remove deleted criteria from DB
foreach (array_keys($currentcriteria) as $id) {
if (!array_key_exists($id, $newcriteria)) {
if ($doupdate) {
$DB->delete_records('gradingform_rubric_criteria', array('id' => $id));
$DB->delete_records('gradingform_rubric_levels', array('criterionid' => $id));
}
$haschanges[3] = true;
}
}
foreach (array('status', 'description', 'descriptionformat', 'name', 'options') as $key) {
if (isset($newdefinition->$key) && $newdefinition->$key != $this->definition->$key) {
$haschanges[1] = true;
}
}
if ($usermodified && $usermodified != $this->definition->usermodified) {
$haschanges[1] = true;
}
if (!count($haschanges)) {
return 0;
}
if ($doupdate) {
parent::update_definition($newdefinition, $usermodified);
$this->load_definition();
}
// return the maximum level of changes
$changelevels = array_keys($haschanges);
sort($changelevels);
return array_pop($changelevels);
}
/**
* Marks all instances filled with this rubric with the status INSTANCE_STATUS_NEEDUPDATE
*/
public function mark_for_regrade() {
global $DB;
if ($this->has_active_instances()) {
$conditions = array('definitionid' => $this->definition->id,
'status' => gradingform_instance::INSTANCE_STATUS_ACTIVE);
$DB->set_field('grading_instances', 'status', gradingform_instance::INSTANCE_STATUS_NEEDUPDATE, $conditions);
}
}
/**
* Loads the rubric form definition if it exists
*
* There is a new array called 'rubric_criteria' appended to the list of parent's definition properties.
*/
protected function load_definition() {
global $DB;
$sql = "SELECT gd.*,
rc.id AS rcid, rc.sortorder AS rcsortorder, rc.description AS rcdescription, rc.descriptionformat AS rcdescriptionformat,
rl.id AS rlid, rl.score AS rlscore, rl.definition AS rldefinition, rl.definitionformat AS rldefinitionformat
FROM {grading_definitions} gd
LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = gd.id)
LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)
WHERE gd.areaid = :areaid AND gd.method = :method
ORDER BY rc.sortorder,rl.score";
$params = array('areaid' => $this->areaid, 'method' => $this->get_method_name());
$rs = $DB->get_recordset_sql($sql, $params);
$this->definition = false;
foreach ($rs as $record) {
// pick the common definition data
if ($this->definition === false) {
$this->definition = new stdClass();
foreach (array('id', 'name', 'description', 'descriptionformat', 'status', 'copiedfromid',
'timecreated', 'usercreated', 'timemodified', 'usermodified', 'timecopied', 'options') as $fieldname) {
$this->definition->$fieldname = $record->$fieldname;
}
$this->definition->rubric_criteria = array();
}
// pick the criterion data
if (!empty($record->rcid) and empty($this->definition->rubric_criteria[$record->rcid])) {
foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) {
$this->definition->rubric_criteria[$record->rcid][$fieldname] = $record->{'rc'.$fieldname};
}
$this->definition->rubric_criteria[$record->rcid]['levels'] = array();
}
// pick the level data
if (!empty($record->rlid)) {
foreach (array('id', 'score', 'definition', 'definitionformat') as $fieldname) {
$value = $record->{'rl'.$fieldname};
if ($fieldname == 'score') {
$value = (float)$value; // To prevent display like 1.00000
}
$this->definition->rubric_criteria[$record->rcid]['levels'][$record->rlid][$fieldname] = $value;
}
}
}
$rs->close();
$options = $this->get_options();
if (!$options['sortlevelsasc']) {
foreach (array_keys($this->definition->rubric_criteria) as $rcid) {
$this->definition->rubric_criteria[$rcid]['levels'] = array_reverse($this->definition->rubric_criteria[$rcid]['levels'], true);
}
}
}
/**
* Returns the default options for the rubric display
*
* @return array
*/
public static function get_default_options() {
$options = array(
'sortlevelsasc' => 1,
'lockzeropoints' => 1,
'alwaysshowdefinition' => 1,
'showdescriptionteacher' => 1,
'showdescriptionstudent' => 1,
'showscoreteacher' => 1,
'showscorestudent' => 1,
'enableremarks' => 1,
'showremarksstudent' => 1
);
return $options;
}
/**
* Gets the options of this rubric definition, fills the missing options with default values
*
* The only exception is 'lockzeropoints' - if other options are present in the json string but this
* one is absent, this means that the rubric was created before Moodle 3.2 and the 0 value should be used.
*
* @return array
*/
public function get_options() {
$options = self::get_default_options();
if (!empty($this->definition->options)) {
$thisoptions = json_decode($this->definition->options, true); // Assoc. array is expected.
foreach ($thisoptions as $option => $value) {
$options[$option] = $value;
}
if (!array_key_exists('lockzeropoints', $thisoptions)) {
// Rubrics created before Moodle 3.2 don't have 'lockzeropoints' option. In this case they should not
// assume default value 1 but use "legacy" value 0.
$options['lockzeropoints'] = 0;
}
}
return $options;
}
/**
* Converts the current definition into an object suitable for the editor form's set_data()
*
* @param boolean $addemptycriterion whether to add an empty criterion if the rubric is completely empty (just being created)
* @return stdClass
*/
public function get_definition_for_editing($addemptycriterion = false) {
$definition = $this->get_definition();
$properties = new stdClass();
$properties->areaid = $this->areaid;
if ($definition) {
foreach (array('id', 'name', 'description', 'descriptionformat', 'status') as $key) {
$properties->$key = $definition->$key;
}
$options = self::description_form_field_options($this->get_context());
$properties = file_prepare_standard_editor($properties, 'description', $options, $this->get_context(),
'grading', 'description', $definition->id);
}
$properties->rubric = array('criteria' => array(), 'options' => $this->get_options());
if (!empty($definition->rubric_criteria)) {
$properties->rubric['criteria'] = $definition->rubric_criteria;
} else if (!$definition && $addemptycriterion) {
$properties->rubric['criteria'] = array('addcriterion' => 1);
}
return $properties;
}
/**
* Returns the form definition suitable for cloning into another area
*
* @see parent::get_definition_copy()
* @param gradingform_controller $target the controller of the new copy
* @return stdClass definition structure to pass to the target's {@link update_definition()}
*/
public function get_definition_copy(gradingform_controller $target) {
$new = parent::get_definition_copy($target);
$old = $this->get_definition_for_editing();
$new->description_editor = $old->description_editor;
$new->rubric = array('criteria' => array(), 'options' => $old->rubric['options']);
$newcritid = 1;
$newlevid = 1;
foreach ($old->rubric['criteria'] as $oldcritid => $oldcrit) {
unset($oldcrit['id']);
if (isset($oldcrit['levels'])) {
foreach ($oldcrit['levels'] as $oldlevid => $oldlev) {
unset($oldlev['id']);
$oldcrit['levels']['NEWID'.$newlevid] = $oldlev;
unset($oldcrit['levels'][$oldlevid]);
$newlevid++;
}
} else {
$oldcrit['levels'] = array();
}
$new->rubric['criteria']['NEWID'.$newcritid] = $oldcrit;
$newcritid++;
}
return $new;
}
/**
* Options for displaying the rubric description field in the form
*
* @param object $context
* @return array options for the form description field
*/
public static function description_form_field_options($context) {
global $CFG;
return array(
'maxfiles' => -1,
'maxbytes' => get_user_max_upload_file_size($context, $CFG->maxbytes),
'context' => $context,
);
}
/**
* Formats the definition description for display on page
*
* @return string
*/
public function get_formatted_description() {
if (!isset($this->definition->description)) {
return '';
}
$context = $this->get_context();
$options = self::description_form_field_options($this->get_context());
$description = file_rewrite_pluginfile_urls($this->definition->description, 'pluginfile.php', $context->id,
'grading', 'description', $this->definition->id, $options);
$formatoptions = array(
'noclean' => false,
'trusted' => false,
'filter' => true,
'context' => $context
);
return format_text($description, $this->definition->descriptionformat, $formatoptions);
}
/**
* Returns the rubric plugin renderer
*
* @param moodle_page $page the target page
* @return gradingform_rubric_renderer
*/
public function get_renderer(moodle_page $page) {
return $page->get_renderer('gradingform_'. $this->get_method_name());
}
/**
* Returns the HTML code displaying the preview of the grading form
*
* @param moodle_page $page the target page
* @return string
*/
public function render_preview(moodle_page $page) {
if (!$this->is_form_defined()) {
throw new coding_exception('It is the caller\'s responsibility to make sure that the form is actually defined');
}
$criteria = $this->definition->rubric_criteria;
$options = $this->get_options();
$rubric = '';
if (has_capability('moodle/grade:managegradingforms', $page->context)) {
$showdescription = true;
} else {
if (empty($options['alwaysshowdefinition'])) {
// ensure we don't display unless show rubric option enabled
return '';
}
$showdescription = $options['showdescriptionstudent'];
}
$output = $this->get_renderer($page);
if ($showdescription) {
$rubric .= $output->box($this->get_formatted_description(), 'gradingform_rubric-description');
}
if (has_capability('moodle/grade:managegradingforms', $page->context)) {
if (!$options['lockzeropoints']) {
// Warn about using grade calculation method where minimum number of points is flexible.
$rubric .= $output->display_rubric_mapping_explained($this->get_min_max_score());
}
$rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric');
} else {
$rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW_GRADED, 'rubric');
}
return $rubric;
}
/**
* Deletes the rubric definition and all the associated information
*/
protected function delete_plugin_definition() {
global $DB;
// get the list of instances
$instances = array_keys($DB->get_records('grading_instances', array('definitionid' => $this->definition->id), '', 'id'));
// delete all fillings
$DB->delete_records_list('gradingform_rubric_fillings', 'instanceid', $instances);
// delete instances
$DB->delete_records_list('grading_instances', 'id', $instances);
// get the list of criteria records
$criteria = array_keys($DB->get_records('gradingform_rubric_criteria', array('definitionid' => $this->definition->id), '', 'id'));
// delete levels
$DB->delete_records_list('gradingform_rubric_levels', 'criterionid', $criteria);
// delete critera
$DB->delete_records_list('gradingform_rubric_criteria', 'id', $criteria);
}
/**
* If instanceid is specified and grading instance exists and it is created by this rater for
* this item, this instance is returned.
* If there exists a draft for this raterid+itemid, take this draft (this is the change from parent)
* Otherwise new instance is created for the specified rater and itemid
*
* @param int $instanceid
* @param int $raterid
* @param int $itemid
* @return gradingform_instance
*/
public function get_or_create_instance($instanceid, $raterid, $itemid) {
global $DB;
if ($instanceid &&
$instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) {
return $this->get_instance($instance);
}
if ($itemid && $raterid) {
$params = array('definitionid' => $this->definition->id, 'raterid' => $raterid, 'itemid' => $itemid);
if ($rs = $DB->get_records('grading_instances', $params, 'timemodified DESC', '*', 0, 1)) {
$record = reset($rs);
$currentinstance = $this->get_current_instance($raterid, $itemid);
if ($record->status == gradingform_rubric_instance::INSTANCE_STATUS_INCOMPLETE &&
(!$currentinstance || $record->timemodified > $currentinstance->get_data('timemodified'))) {
$record->isrestored = true;
return $this->get_instance($record);
}
}
}
return $this->create_instance($raterid, $itemid);
}
/**
* Returns html code to be included in student's feedback.
*
* @param moodle_page $page
* @param int $itemid
* @param array $gradinginfo result of function grade_get_grades
* @param string $defaultcontent default string to be returned if no active grading is found
* @param boolean $cangrade whether current user has capability to grade in this context
* @return string
*/
public function render_grade($page, $itemid, $gradinginfo, $defaultcontent, $cangrade) {
return $this->get_renderer($page)->display_instances($this->get_active_instances($itemid), $defaultcontent, $cangrade);
}
// ///// full-text search support /////////////////////////////////////////////
/**
* Prepare the part of the search query to append to the FROM statement
*
* @param string $gdid the alias of grading_definitions.id column used by the caller
* @return string
*/
public static function sql_search_from_tables($gdid) {
return " LEFT JOIN {gradingform_rubric_criteria} rc ON (rc.definitionid = $gdid)
LEFT JOIN {gradingform_rubric_levels} rl ON (rl.criterionid = rc.id)";
}
/**
* Prepare the parts of the SQL WHERE statement to search for the given token
*
* The returned array cosists of the list of SQL comparions and the list of
* respective parameters for the comparisons. The returned chunks will be joined
* with other conditions using the OR operator.
*
* @param string $token token to search for
* @return array
*/
public static function sql_search_where($token) {
global $DB;
$subsql = array();
$params = array();
// search in rubric criteria description
$subsql[] = $DB->sql_like('rc.description', '?', false, false);
$params[] = '%'.$DB->sql_like_escape($token).'%';
// search in rubric levels definition
$subsql[] = $DB->sql_like('rl.definition', '?', false, false);
$params[] = '%'.$DB->sql_like_escape($token).'%';
return array($subsql, $params);
}
/**
* Calculates and returns the possible minimum and maximum score (in points) for this rubric
*
* @return array
*/
public function get_min_max_score() {
if (!$this->is_form_available()) {
return null;
}
$returnvalue = array('minscore' => 0, 'maxscore' => 0);
foreach ($this->get_definition()->rubric_criteria as $id => $criterion) {
$scores = array();
foreach ($criterion['levels'] as $level) {
$scores[] = $level['score'];
}
sort($scores);
$returnvalue['minscore'] += $scores[0];
$returnvalue['maxscore'] += $scores[sizeof($scores)-1];
}
return $returnvalue;
}
/**
* @return array An array containing a single key/value pair with the 'rubric_criteria' external_multiple_structure.
* @see gradingform_controller::get_external_definition_details()
* @since Moodle 2.5
*/
public static function get_external_definition_details() {
$rubric_criteria = new external_multiple_structure(
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'criterion id', VALUE_OPTIONAL),
'sortorder' => new external_value(PARAM_INT, 'sortorder', VALUE_OPTIONAL),
'description' => new external_value(PARAM_RAW, 'description', VALUE_OPTIONAL),
'descriptionformat' => new external_format_value('description', VALUE_OPTIONAL),
'levels' => new external_multiple_structure(
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL),
'score' => new external_value(PARAM_FLOAT, 'score', VALUE_OPTIONAL),
'definition' => new external_value(PARAM_RAW, 'definition', VALUE_OPTIONAL),
'definitionformat' => new external_format_value('definition', VALUE_OPTIONAL)
)
), 'levels', VALUE_OPTIONAL
)
)
), 'definition details', VALUE_OPTIONAL
);
return array('rubric_criteria' => $rubric_criteria);
}
/**
* Returns an array that defines the structure of the rubric's filling. This function is used by
* the web service function core_grading_external::get_gradingform_instances().
*
* @return An array containing a single key/value pair with the 'criteria' external_multiple_structure
* @see gradingform_controller::get_external_instance_filling_details()
* @since Moodle 2.6
*/
public static function get_external_instance_filling_details() {
$criteria = new external_multiple_structure(
new external_single_structure(
array(
'id' => new external_value(PARAM_INT, 'filling id'),
'criterionid' => new external_value(PARAM_INT, 'criterion id'),
'levelid' => new external_value(PARAM_INT, 'level id', VALUE_OPTIONAL),
'remark' => new external_value(PARAM_RAW, 'remark', VALUE_OPTIONAL),
'remarkformat' => new external_format_value('remark', VALUE_OPTIONAL)
)
), 'filling', VALUE_OPTIONAL
);
return array ('criteria' => $criteria);
}
}
/**
* Class to manage one rubric grading instance.
*
* Stores information and performs actions like update, copy, validate, submit, etc.
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class gradingform_rubric_instance extends gradingform_instance {
/** @var array stores the rubric, has two keys: 'criteria' and 'options' */
protected $rubric;
/**
* Deletes this (INCOMPLETE) instance from database.
*/
public function cancel() {
global $DB;
parent::cancel();
$DB->delete_records('gradingform_rubric_fillings', array('instanceid' => $this->get_id()));
}
/**
* Duplicates the instance before editing (optionally substitutes raterid and/or itemid with
* the specified values)
*
* @param int $raterid value for raterid in the duplicate
* @param int $itemid value for itemid in the duplicate
* @return int id of the new instance
*/
public function copy($raterid, $itemid) {
global $DB;
$instanceid = parent::copy($raterid, $itemid);
$currentgrade = $this->get_rubric_filling();
foreach ($currentgrade['criteria'] as $criterionid => $record) {
$params = array('instanceid' => $instanceid, 'criterionid' => $criterionid,
'levelid' => $record['levelid'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']);
$DB->insert_record('gradingform_rubric_fillings', $params);
}
return $instanceid;
}
/**
* Determines whether the submitted form was empty.
*
* @param array $elementvalue value of element submitted from the form
* @return boolean true if the form is empty
*/
public function is_empty_form($elementvalue) {
$criteria = $this->get_controller()->get_definition()->rubric_criteria;
foreach ($criteria as $id => $criterion) {
if (isset($elementvalue['criteria'][$id]['levelid'])
|| !empty($elementvalue['criteria'][$id]['remark'])) {
return false;
}
}
return true;
}
/**
* Removes the attempt from the gradingform_guide_fillings table
* @param array $data the attempt data
*/
public function clear_attempt($data) {
global $DB;
foreach ($data['criteria'] as $criterionid => $record) {
$DB->delete_records('gradingform_rubric_fillings',
array('criterionid' => $criterionid, 'instanceid' => $this->get_id()));
}
}
/**
* Validates that rubric is fully completed and contains valid grade on each criterion
*
* @param array $elementvalue value of element as came in form submit
* @return boolean true if the form data is validated and contains no errors
*/
public function validate_grading_element($elementvalue) {
$criteria = $this->get_controller()->get_definition()->rubric_criteria;
if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) || sizeof($elementvalue['criteria']) < sizeof($criteria)) {
return false;
}
foreach ($criteria as $id => $criterion) {
if (!isset($elementvalue['criteria'][$id]['levelid'])
|| !array_key_exists($elementvalue['criteria'][$id]['levelid'], $criterion['levels'])) {
return false;
}
}
return true;
}
/**
* Retrieves from DB and returns the data how this rubric was filled
*
* @param boolean $force whether to force DB query even if the data is cached
* @return array
*/
public function get_rubric_filling($force = false) {
global $DB;
if ($this->rubric === null || $force) {
$records = $DB->get_records('gradingform_rubric_fillings', array('instanceid' => $this->get_id()));
$this->rubric = array('criteria' => array());
foreach ($records as $record) {
$this->rubric['criteria'][$record->criterionid] = (array)$record;
}
}
return $this->rubric;
}
/**
* Updates the instance with the data received from grading form. This function may be
* called via AJAX when grading is not yet completed, so it does not change the
* status of the instance.
*
* @param array $data
*/
public function update($data) {
global $DB;
$currentgrade = $this->get_rubric_filling();
parent::update($data);
foreach ($data['criteria'] as $criterionid => $record) {
if (!array_key_exists($criterionid, $currentgrade['criteria'])) {
$newrecord = array('instanceid' => $this->get_id(), 'criterionid' => $criterionid,
'levelid' => $record['levelid'], 'remarkformat' => FORMAT_MOODLE);
if (isset($record['remark'])) {
$newrecord['remark'] = $record['remark'];
}
$DB->insert_record('gradingform_rubric_fillings', $newrecord);
} else {
$newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']);
foreach (array('levelid', 'remark'/*, 'remarkformat' */) as $key) {
// TODO MDL-31235 format is not supported yet
if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) {
$newrecord[$key] = $record[$key];
}
}
if (count($newrecord) > 1) {
$DB->update_record('gradingform_rubric_fillings', $newrecord);
}
}
}
foreach ($currentgrade['criteria'] as $criterionid => $record) {
if (!array_key_exists($criterionid, $data['criteria'])) {
$DB->delete_records('gradingform_rubric_fillings', array('id' => $record['id']));
}
}
$this->get_rubric_filling(true);
}
/**
* Calculates the grade to be pushed to the gradebook
*
* @return float|int the valid grade from $this->get_controller()->get_grade_range()
*/
public function get_grade() {
$grade = $this->get_rubric_filling();
if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) {
return -1;
}
$graderange = array_keys($this->get_controller()->get_grade_range());
if (empty($graderange)) {
return -1;
}
sort($graderange);
$mingrade = $graderange[0];
$maxgrade = $graderange[sizeof($graderange) - 1];
$curscore = 0;
foreach ($grade['criteria'] as $id => $record) {
$curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score'];
}
$allowdecimals = $this->get_controller()->get_allow_grade_decimals();
$options = $this->get_controller()->get_options();
if ($options['lockzeropoints']) {
// Grade calculation method when 0-level is locked.
$grade = max($mingrade, $curscore / $scores['maxscore'] * $maxgrade);
return $allowdecimals ? $grade : round($grade, 0);
} else {
// Alternative grade calculation method.
$gradeoffset = ($curscore - $scores['minscore']) / ($scores['maxscore'] - $scores['minscore']) * ($maxgrade - $mingrade);
return ($allowdecimals ? $gradeoffset : round($gradeoffset, 0)) + $mingrade;
}
}
/**
* Returns html for form element of type 'grading'.
*
* @param moodle_page $page
* @param MoodleQuickForm_grading $gradingformelement
* @return string
*/
public function render_grading_element($page, $gradingformelement) {
global $USER;
if (!$gradingformelement->_flagFrozen) {
$module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js');
$page->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName())), true, $module);
$mode = gradingform_rubric_controller::DISPLAY_EVAL;
} else {
if ($gradingformelement->_persistantFreeze) {
$mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN;
} else {
$mode = gradingform_rubric_controller::DISPLAY_REVIEW;
}
}
$criteria = $this->get_controller()->get_definition()->rubric_criteria;
$options = $this->get_controller()->get_options();
$value = $gradingformelement->getValue();
$html = '';
if ($value === null) {
$value = $this->get_rubric_filling();
} else if (!$this->validate_grading_element($value)) {
$html .= html_writer::tag('div', get_string('rubricnotcompleted', 'gradingform_rubric'), array('class' => 'gradingform_rubric-error'));
}
$currentinstance = $this->get_current_instance();
if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) {
$html .= html_writer::div(get_string('needregrademessage', 'gradingform_rubric'), 'gradingform_rubric-regrade',
array('role' => 'alert'));
}
$haschanges = false;
if ($currentinstance) {
$curfilling = $currentinstance->get_rubric_filling();
foreach ($curfilling['criteria'] as $criterionid => $curvalues) {
$value['criteria'][$criterionid]['savedlevelid'] = $curvalues['levelid'];
$newremark = null;
$newlevelid = null;
if (isset($value['criteria'][$criterionid]['remark'])) $newremark = $value['criteria'][$criterionid]['remark'];
if (isset($value['criteria'][$criterionid]['levelid'])) $newlevelid = $value['criteria'][$criterionid]['levelid'];
if ($newlevelid != $curvalues['levelid'] || $newremark != $curvalues['remark']) {
$haschanges = true;
}
}
}
if ($this->get_data('isrestored') && $haschanges) {
$html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_rubric'), array('class' => 'gradingform_rubric-restored'));
}
if (!empty($options['showdescriptionteacher'])) {
$html .= html_writer::tag('div', $this->get_controller()->get_formatted_description(), array('class' => 'gradingform_rubric-description'));
}
$html .= $this->get_controller()->get_renderer($page)->display_rubric($criteria, $options, $mode, $gradingformelement->getName(), $value);
return $html;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

+3
View File
@@ -0,0 +1,3 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M15 0H1C.5 0 0 .5 0 1v14c0 .5.5 1 1 1h14c.5 0 1-.5 1-1V1c0-.5-.5-1-1-1zM8 14H2v-2h6v2zm0-3H2V9h6v2zm0-3H2V6h6v2zm3 6H9v-2h2v2zm0-3H9V9h2v2zm0-3H9V6h2v2zm3 6h-2v-2h2v2zm0-3h-2V9h2v2zm0-3h-2V6h2v2z" fill="#888"/></svg>

After

Width:  |  Height:  |  Size: 518 B

+53
View File
@@ -0,0 +1,53 @@
<?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/>.
/**
* Preview rubric page
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__.'/../../../../config.php');
require_once(__DIR__.'/lib.php');
require_once(__DIR__.'/edit_form.php');
require_once($CFG->dirroot.'/grade/grading/lib.php');
$areaid = required_param('areaid', PARAM_INT);
$manager = get_grading_manager($areaid);
list($context, $course, $cm) = get_context_info_array($manager->get_context()->id);
require_login($course, true, $cm);
$controller = $manager->get_controller('rubric');
$options = $controller->get_options();
if (!$controller->is_form_defined() || empty($options['alwaysshowdefinition'])) {
throw new moodle_exception('nopermissions', 'error', '', get_string('previewrubric', 'gradingform_rubric'));
}
$title = get_string('gradingof', 'gradingform_rubric', $manager->get_area_title());
$PAGE->set_url(new moodle_url('/grade/grading/form/rubric/preview.php', array('areaid' => $areaid)));
$PAGE->set_title($title);
$PAGE->set_heading($title);
echo $OUTPUT->header();
echo $OUTPUT->heading($title);
echo $controller->render_preview($PAGE);
echo $OUTPUT->footer();
+658
View File
@@ -0,0 +1,658 @@
<?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/>.
/**
* Contains renderer used for displaying rubric
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Grading method plugin renderer
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class gradingform_rubric_renderer extends plugin_renderer_base {
/**
* This function returns html code for displaying criterion. Depending on $mode it may be the
* code to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation.
*
* This function may be called from display_rubric() to display the whole rubric, or it can be
* called by itself to return a template used by JavaScript to add new empty criteria to the
* rubric being designed.
* In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc.
*
* When overriding this function it is very important to remember that all elements of html
* form (in edit or evaluate mode) must have the name $elementname.
*
* Also JavaScript relies on the class names of elements and when developer changes them
* script might stop working.
*
* @param int $mode rubric display mode, see {@link gradingform_rubric_controller}
* @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()}
* @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
* @param array|null $criterion criterion data
* @param string $levelsstr evaluated templates for this criterion levels
* @param array|null $value (only in view mode) teacher's feedback on this criterion
* @return string
*/
public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $levelsstr = '{LEVELS}', $value = null) {
// TODO MDL-31235 description format, remark format
if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) {
$criterion = array('id' => '{CRITERION-id}', 'description' => '{CRITERION-description}', 'sortorder' => '{CRITERION-sortorder}', 'class' => '{CRITERION-class}');
} else {
foreach (array('sortorder', 'description', 'class') as $key) {
// set missing array elements to empty strings to avoid warnings
if (!array_key_exists($key, $criterion)) {
$criterion[$key] = '';
}
}
}
$criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'], 'id' => '{NAME}-criteria-{CRITERION-id}'));
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
$criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls'));
foreach (array('moveup', 'delete', 'movedown', 'duplicate') as $key) {
$value = get_string('criterion'.$key, 'gradingform_rubric');
$button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']',
'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value));
$criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
}
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]',
'value' => $criterion['sortorder']));
$criteriontemplate .= html_writer::end_tag('td'); // .controls
// Criterion description text area.
$descriptiontextareaparams = array(
'name' => '{NAME}[criteria][{CRITERION-id}][description]',
'id' => '{NAME}-criteria-{CRITERION-id}-description',
'aria-label' => get_string('criterion', 'gradingform_rubric', ''),
'cols' => '10', 'rows' => '5'
);
$description = html_writer::tag('textarea', s($criterion['description']), $descriptiontextareaparams);
} else {
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'value' => $criterion['description']));
}
$description = s($criterion['description']);
}
$descriptionclass = 'description';
if (isset($criterion['error_description'])) {
$descriptionclass .= ' error';
}
// Description cell params.
$descriptiontdparams = array(
'class' => $descriptionclass,
'id' => '{NAME}-criteria-{CRITERION-id}-description-cell'
);
if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL &&
$mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
// Set description's cell as tab-focusable.
$descriptiontdparams['tabindex'] = '0';
// Set label for the criterion cell.
$descriptiontdparams['aria-label'] = get_string('criterion', 'gradingform_rubric', s($criterion['description']));
}
// Description cell.
$criteriontemplate .= html_writer::tag('td', $description, $descriptiontdparams);
// Levels table.
$levelsrowparams = [
'id' => '{NAME}-criteria-{CRITERION-id}-levels',
'aria-label' => get_string('levelsgroup', 'gradingform_rubric'),
];
// Add radiogroup role only when not previewing or editing.
$isradiogroup = !in_array($mode, [
gradingform_rubric_controller::DISPLAY_EDIT_FULL,
gradingform_rubric_controller::DISPLAY_EDIT_FROZEN,
gradingform_rubric_controller::DISPLAY_PREVIEW,
gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED,
]);
$levelsrowparams['role'] = $isradiogroup ? 'radiogroup' : 'list';
$levelsrow = html_writer::tag('tr', $levelsstr, $levelsrowparams);
$levelstableparams = [
'id' => '{NAME}-criteria-{CRITERION-id}-levels-table',
'role' => 'none',
];
$levelsstrtable = html_writer::tag('table', $levelsrow, $levelstableparams);
$levelsclass = 'levels';
if (isset($criterion['error_levels'])) {
$levelsclass .= ' error';
}
$criteriontemplate .= html_writer::tag('td', $levelsstrtable, array('class' => $levelsclass));
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
$value = get_string('criterionaddlevel', 'gradingform_rubric');
$button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][addlevel]',
'id' => '{NAME}-criteria-{CRITERION-id}-levels-addlevel', 'value' => $value, 'class' => 'btn btn-secondary'));
$criteriontemplate .= html_writer::tag('td', $button, array('class' => 'addlevel'));
}
$displayremark = ($options['enableremarks'] && ($mode != gradingform_rubric_controller::DISPLAY_VIEW || $options['showremarksstudent']));
if ($displayremark) {
$currentremark = '';
if (isset($value['remark'])) {
$currentremark = $value['remark'];
}
// Label for criterion remark.
$remarkinfo = new stdClass();
$remarkinfo->description = s($criterion['description']);
$remarkinfo->remark = $currentremark;
$remarklabeltext = get_string('criterionremark', 'gradingform_rubric', $remarkinfo);
if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
// HTML parameters for remarks text area.
$remarkparams = array(
'name' => '{NAME}[criteria][{CRITERION-id}][remark]',
'id' => '{NAME}-criteria-{CRITERION-id}-remark',
'cols' => '10', 'rows' => '5',
'aria-label' => $remarklabeltext
);
$input = html_writer::tag('textarea', s($currentremark), $remarkparams);
$criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark'));
} else if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN) {
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
}else if ($mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW) {
// HTML parameters for remarks cell.
$remarkparams = array(
'class' => 'remark',
'tabindex' => '0',
'id' => '{NAME}-criteria-{CRITERION-id}-remark',
'aria-label' => $remarklabeltext
);
$criteriontemplate .= html_writer::tag('td', s($currentremark), $remarkparams);
}
}
$criteriontemplate .= html_writer::end_tag('tr'); // .criterion
$criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate);
$criteriontemplate = str_replace('{CRITERION-id}', $criterion['id'], $criteriontemplate);
return $criteriontemplate;
}
/**
* This function returns html code for displaying one level of one criterion. Depending on $mode
* it may be the code to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation.
*
* This function may be called from display_rubric() to display the whole rubric, or it can be
* called by itself to return a template used by JavaScript to add new empty level to the
* criterion during the design of rubric.
* In this case it will use macros like {NAME}, {CRITERION-id}, {LEVEL-id}, etc.
*
* When overriding this function it is very important to remember that all elements of html
* form (in edit or evaluate mode) must have the name $elementname.
*
* Also JavaScript relies on the class names of elements and when developer changes them
* script might stop working.
*
* @param int $mode rubric display mode see {@link gradingform_rubric_controller}
* @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()}
* @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
* @param string|int $criterionid either id of the nesting criterion or a macro for template
* @param array|null $level level data, also in view mode it might also have property $level['checked'] whether this level is checked
* @return string
*/
public function level_template($mode, $options, $elementname = '{NAME}', $criterionid = '{CRITERION-id}', $level = null) {
// TODO MDL-31235 definition format
if (!isset($level['id'])) {
$level = array('id' => '{LEVEL-id}', 'definition' => '{LEVEL-definition}', 'score' => '{LEVEL-score}', 'class' => '{LEVEL-class}', 'checked' => false);
} else {
foreach (array('score', 'definition', 'class', 'checked', 'index') as $key) {
// set missing array elements to empty strings to avoid warnings
if (!array_key_exists($key, $level)) {
$level[$key] = '';
}
}
}
// Get level index.
$levelindex = isset($level['index']) ? $level['index'] : '{LEVEL-index}';
// Template for one level within one criterion
$tdattributes = array(
'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}',
'class' => 'text-break level' . $level['class']
);
if (isset($level['tdwidth'])) {
$tdattributes['style'] = "width: " . round($level['tdwidth']).'%;';
}
$leveltemplate = html_writer::start_tag('div', array('class' => 'level-wrapper'));
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
$definitionparams = array(
'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition',
'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]',
'aria-label' => get_string('leveldefinition', 'gradingform_rubric', $levelindex),
'cols' => '10', 'rows' => '4'
);
$definition = html_writer::tag('textarea', s($level['definition']), $definitionparams);
$scoreparams = array(
'type' => 'text',
'id' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]',
'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]',
'aria-label' => get_string('scoreinputforlevel', 'gradingform_rubric', $levelindex),
'size' => '3',
'value' => $level['score']
);
$score = html_writer::empty_tag('input', $scoreparams);
} else {
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
$leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition']));
$leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'value' => $level['score']));
}
$definition = s($level['definition']);
$score = $level['score'];
}
if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) {
$levelradioparams = array(
'type' => 'radio',
'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition',
'name' => '{NAME}[criteria][{CRITERION-id}][levelid]',
'value' => $level['id']
);
if ($level['checked']) {
$levelradioparams['checked'] = 'checked';
}
$input = html_writer::empty_tag('input', $levelradioparams);
$leveltemplate .= html_writer::div($input, 'radio');
}
if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) {
$leveltemplate .= html_writer::empty_tag('input',
array(
'type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][levelid]',
'value' => $level['id']
)
);
}
$score = html_writer::tag('span', $score, array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-score', 'class' => 'scorevalue'));
$definitionclass = 'definition';
if (isset($level['error_definition'])) {
$definitionclass .= ' error';
}
if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL &&
$mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
$tdattributes['tabindex'] = '0';
$levelinfo = new stdClass();
$levelinfo->definition = s($level['definition']);
$levelinfo->score = $level['score'];
$tdattributes['aria-label'] = get_string('level', 'gradingform_rubric', $levelinfo);
if ($mode != gradingform_rubric_controller::DISPLAY_PREVIEW &&
$mode != gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED) {
// Add role of radio button to level cell if not in edit and preview mode.
$tdattributes['role'] = 'radio';
if ($level['checked']) {
$tdattributes['aria-checked'] = 'true';
} else {
$tdattributes['aria-checked'] = 'false';
}
} else {
$tdattributes['role'] = 'listitem';
}
} else {
$tdattributes['role'] = 'listitem';
}
$leveltemplateparams = array(
'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-definition-container'
);
$leveltemplate .= html_writer::div($definition, $definitionclass, $leveltemplateparams);
$displayscore = true;
if (!$options['showscoreteacher'] && in_array($mode, array(gradingform_rubric_controller::DISPLAY_EVAL, gradingform_rubric_controller::DISPLAY_EVAL_FROZEN, gradingform_rubric_controller::DISPLAY_REVIEW))) {
$displayscore = false;
}
if (!$options['showscorestudent'] && in_array($mode, array(gradingform_rubric_controller::DISPLAY_VIEW, gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED))) {
$displayscore = false;
}
if ($displayscore) {
$scoreclass = 'score d-inline';
if (isset($level['error_score'])) {
$scoreclass .= ' error';
}
$leveltemplate .= html_writer::tag('div', get_string('scorepostfix', 'gradingform_rubric', $score), array('class' => $scoreclass));
}
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
$value = get_string('leveldelete', 'gradingform_rubric', $levelindex);
$buttonparams = array(
'type' => 'submit',
'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][delete]',
'id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-delete',
'value' => $value
);
$button = html_writer::empty_tag('input', $buttonparams);
$leveltemplate .= html_writer::tag('div', $button, array('class' => 'delete'));
}
$leveltemplate .= html_writer::end_tag('div'); // .level-wrapper
$leveltemplate = html_writer::tag('td', $leveltemplate, $tdattributes); // The .level cell.
$leveltemplate = str_replace('{NAME}', $elementname, $leveltemplate);
$leveltemplate = str_replace('{CRITERION-id}', $criterionid, $leveltemplate);
$leveltemplate = str_replace('{LEVEL-id}', $level['id'], $leveltemplate);
return $leveltemplate;
}
/**
* This function returns html code for displaying rubric template (content before and after
* criteria list). Depending on $mode it may be the code to edit rubric, to preview the rubric,
* to evaluate somebody or to review the evaluation.
*
* This function is called from display_rubric() to display the whole rubric.
*
* When overriding this function it is very important to remember that all elements of html
* form (in edit or evaluate mode) must have the name $elementname.
*
* Also JavaScript relies on the class names of elements and when developer changes them
* script might stop working.
*
* @param int $mode rubric display mode see {@link gradingform_rubric_controller}
* @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()}
* @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
* @param string $criteriastr evaluated templates for this rubric's criteria
* @return string
*/
protected function rubric_template($mode, $options, $elementname, $criteriastr) {
$classsuffix = ''; // CSS suffix for class of the main div. Depends on the mode
switch ($mode) {
case gradingform_rubric_controller::DISPLAY_EDIT_FULL:
$classsuffix = ' editor editable'; break;
case gradingform_rubric_controller::DISPLAY_EDIT_FROZEN:
$classsuffix = ' editor frozen'; break;
case gradingform_rubric_controller::DISPLAY_PREVIEW:
case gradingform_rubric_controller::DISPLAY_PREVIEW_GRADED:
$classsuffix = ' editor preview'; break;
case gradingform_rubric_controller::DISPLAY_EVAL:
$classsuffix = ' evaluate editable'; break;
case gradingform_rubric_controller::DISPLAY_EVAL_FROZEN:
$classsuffix = ' evaluate frozen'; break;
case gradingform_rubric_controller::DISPLAY_REVIEW:
$classsuffix = ' review'; break;
case gradingform_rubric_controller::DISPLAY_VIEW:
$classsuffix = ' view'; break;
}
$rubrictemplate = html_writer::start_tag('div', array('id' => 'rubric-{NAME}', 'class' => 'clearfix gradingform_rubric'.$classsuffix));
// Rubric table.
$rubrictableparams = [
'class' => 'criteria',
'id' => '{NAME}-criteria',
];
$caption = html_writer::tag('caption', get_string('rubric', 'gradingform_rubric'), ['class' => 'sr-only']);
$rubrictable = html_writer::tag('table', $caption . $criteriastr, $rubrictableparams);
$rubrictemplate .= $rubrictable;
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
$value = get_string('addcriterion', 'gradingform_rubric');
$criteriainputparams = array(
'type' => 'submit',
'name' => '{NAME}[criteria][addcriterion]',
'id' => '{NAME}-criteria-addcriterion',
'value' => $value
);
$input = html_writer::empty_tag('input', $criteriainputparams);
$rubrictemplate .= html_writer::tag('div', $input, array('class' => 'addcriterion btn btn-secondary'));
}
$rubrictemplate .= $this->rubric_edit_options($mode, $options);
$rubrictemplate .= html_writer::end_tag('div');
return str_replace('{NAME}', $elementname, $rubrictemplate);
}
/**
* Generates html template to view/edit the rubric options. Expression {NAME} is used in
* template for the form element name
*
* @param int $mode rubric display mode see {@link gradingform_rubric_controller}
* @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()}
* @return string
*/
protected function rubric_edit_options($mode, $options) {
if ($mode != gradingform_rubric_controller::DISPLAY_EDIT_FULL
&& $mode != gradingform_rubric_controller::DISPLAY_EDIT_FROZEN
&& $mode != gradingform_rubric_controller::DISPLAY_PREVIEW) {
// Options are displayed only for people who can manage
return;
}
$html = html_writer::start_tag('div', array('class' => 'options'));
$html .= html_writer::tag('div', get_string('rubricoptions', 'gradingform_rubric'), array('class' => 'optionsheading'));
$attrs = array('type' => 'hidden', 'name' => '{NAME}[options][optionsset]', 'value' => 1);
foreach ($options as $option => $value) {
$html .= html_writer::start_tag('div', array('class' => 'option '.$option));
$attrs = array('name' => '{NAME}[options]['.$option.']', 'id' => '{NAME}-options-'.$option);
switch ($option) {
case 'sortlevelsasc':
// Display option as dropdown
$html .= html_writer::label(get_string($option, 'gradingform_rubric'), $attrs['id'], false);
$value = (int)(!!$value); // make sure $value is either 0 or 1
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
$selectoptions = array(0 => get_string($option.'0', 'gradingform_rubric'), 1 => get_string($option.'1', 'gradingform_rubric'));
$valuestr = html_writer::select($selectoptions, $attrs['name'], $value, false, array('id' => $attrs['id']));
$html .= html_writer::tag('span', $valuestr, array('class' => 'value'));
} else {
$html .= html_writer::tag('span', get_string($option.$value, 'gradingform_rubric'), array('class' => 'value'));
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
$html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value));
}
}
break;
default:
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN && $value) {
// Id should be different then the actual input added later.
$attrs['id'] .= '_hidden';
$html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value));
}
// Display option as checkbox
$attrs['type'] = 'checkbox';
$attrs['value'] = 1;
if ($value) {
$attrs['checked'] = 'checked';
}
if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN || $mode == gradingform_rubric_controller::DISPLAY_PREVIEW) {
$attrs['disabled'] = 'disabled';
unset($attrs['name']);
// Id should be different then the actual input added later.
$attrs['id'] .= '_disabled';
}
$html .= html_writer::empty_tag('input', $attrs);
$html .= html_writer::tag('label', get_string($option, 'gradingform_rubric'), array('for' => $attrs['id']));
break;
}
if (get_string_manager()->string_exists($option.'_help', 'gradingform_rubric')) {
$html .= $this->help_icon($option, 'gradingform_rubric');
}
$html .= html_writer::end_tag('div'); // .option
}
$html .= html_writer::end_tag('div'); // .options
return $html;
}
/**
* This function returns html code for displaying rubric. Depending on $mode it may be the code
* to edit rubric, to preview the rubric, to evaluate somebody or to review the evaluation.
*
* It is very unlikely that this function needs to be overriden by theme. It does not produce
* any html code, it just prepares data about rubric design and evaluation, adds the CSS
* class to elements and calls the functions level_template, criterion_template and
* rubric_template
*
* @param array $criteria data about the rubric design
* @param array $options display options for this rubric, defaults are: {@link gradingform_rubric_controller::get_default_options()}
* @param int $mode rubric display mode, see {@link gradingform_rubric_controller}
* @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode)
* @param array $values evaluation result
* @return string
*/
public function display_rubric($criteria, $options, $mode, $elementname = null, $values = null) {
$criteriastr = '';
$cnt = 0;
foreach ($criteria as $id => $criterion) {
$criterion['class'] = $this->get_css_class_suffix($cnt++, sizeof($criteria) -1);
$criterion['id'] = $id;
$levelsstr = '';
$levelcnt = 0;
if (isset($values['criteria'][$id])) {
$criterionvalue = $values['criteria'][$id];
} else {
$criterionvalue = null;
}
$index = 1;
foreach ($criterion['levels'] as $levelid => $level) {
$level['id'] = $levelid;
$level['class'] = $this->get_css_class_suffix($levelcnt++, sizeof($criterion['levels']) -1);
$level['checked'] = (isset($criterionvalue['levelid']) && ((int)$criterionvalue['levelid'] === $levelid));
if ($level['checked'] && ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN || $mode == gradingform_rubric_controller::DISPLAY_REVIEW || $mode == gradingform_rubric_controller::DISPLAY_VIEW)) {
$level['class'] .= ' checked';
//in mode DISPLAY_EVAL the class 'checked' will be added by JS if it is enabled. If JS is not enabled, the 'checked' class will only confuse
}
if (isset($criterionvalue['savedlevelid']) && ((int)$criterionvalue['savedlevelid'] === $levelid)) {
$level['class'] .= ' currentchecked';
}
$level['tdwidth'] = 100/count($criterion['levels']);
$level['index'] = $index;
$levelsstr .= $this->level_template($mode, $options, $elementname, $id, $level);
$index++;
}
$criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $levelsstr, $criterionvalue);
}
return $this->rubric_template($mode, $options, $elementname, $criteriastr);
}
/**
* Help function to return CSS class names for element (first/last/even/odd) with leading space
*
* @param int $idx index of this element in the row/column
* @param int $maxidx maximum index of the element in the row/column
* @return string
*/
protected function get_css_class_suffix($idx, $maxidx) {
$class = '';
if ($idx == 0) {
$class .= ' first';
}
if ($idx == $maxidx) {
$class .= ' last';
}
if ($idx%2) {
$class .= ' odd';
} else {
$class .= ' even';
}
return $class;
}
/**
* Displays for the student the list of instances or default content if no instances found
*
* @param array $instances array of objects of type gradingform_rubric_instance
* @param string $defaultcontent default string that would be displayed without advanced grading
* @param boolean $cangrade whether current user has capability to grade in this context
* @return string
*/
public function display_instances($instances, $defaultcontent, $cangrade) {
$return = '';
if (sizeof($instances)) {
$return .= html_writer::start_tag('div', array('class' => 'advancedgrade'));
$idx = 0;
foreach ($instances as $instance) {
$return .= $this->display_instance($instance, $idx++, $cangrade);
}
$return .= html_writer::end_tag('div');
}
return $return. $defaultcontent;
}
/**
* Displays one grading instance
*
* @param gradingform_rubric_instance $instance
* @param int $idx unique number of instance on page
* @param bool $cangrade whether current user has capability to grade in this context
*/
public function display_instance(gradingform_rubric_instance $instance, $idx, $cangrade) {
$criteria = $instance->get_controller()->get_definition()->rubric_criteria;
$options = $instance->get_controller()->get_options();
$values = $instance->get_rubric_filling();
if ($cangrade) {
$mode = gradingform_rubric_controller::DISPLAY_REVIEW;
$showdescription = $options['showdescriptionteacher'];
} else {
$mode = gradingform_rubric_controller::DISPLAY_VIEW;
$showdescription = $options['showdescriptionstudent'];
}
$output = '';
if ($showdescription) {
$output .= $this->box($instance->get_controller()->get_formatted_description(), 'gradingform_rubric-description');
}
$output .= $this->display_rubric($criteria, $options, $mode, 'rubric'.$idx, $values);
return $output;
}
/**
* Displays confirmation that students require re-grading
*
* @param string $elementname
* @param int $changelevel
* @param string $value
* @return string
*/
public function display_regrade_confirmation($elementname, $changelevel, $value) {
$html = html_writer::start_tag('div', array('class' => 'gradingform_rubric-regrade', 'role' => 'alert'));
if ($changelevel<=2) {
$html .= html_writer::label(get_string('regrademessage1', 'gradingform_rubric'), 'menu' . $elementname . 'regrade');
$selectoptions = array(
0 => get_string('regradeoption0', 'gradingform_rubric'),
1 => get_string('regradeoption1', 'gradingform_rubric')
);
$html .= html_writer::select($selectoptions, $elementname.'[regrade]', $value, false);
} else {
$html .= get_string('regrademessage5', 'gradingform_rubric');
$html .= html_writer::empty_tag('input', array('name' => $elementname.'[regrade]', 'value' => 1, 'type' => 'hidden'));
}
$html .= html_writer::end_tag('div');
return $html;
}
/**
* Generates and returns HTML code to display information box about how rubric score is converted to the grade
*
* @param array $scores
* @return string
*/
public function display_rubric_mapping_explained($scores) {
$html = '';
if (!$scores) {
return $html;
}
if ($scores['minscore'] <> 0) {
$html .= $this->output->notification(get_string('zerolevelsabsent', 'gradingform_rubric'), 'error');
}
$html .= $this->output->notification(get_string('rubricmappingexplained', 'gradingform_rubric', (object)$scores), 'info');
return $html;
}
}
+395
View File
@@ -0,0 +1,395 @@
<?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/>.
/**
* File contains definition of class MoodleQuickForm_rubriceditor
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once("HTML/QuickForm/input.php");
/**
* Form element for handling rubric editor
*
* The rubric editor is defined as a separate form element. This allows us to render
* criteria, levels and buttons using the rubric's own renderer. Also, the required
* Javascript library is included, which processes, on the client, buttons needed
* for reordering, adding and deleting criteria.
*
* If Javascript is disabled when one of those special buttons is pressed, the form
* element is not validated and, instead of submitting the form, we process button presses.
*
* @package gradingform_rubric
* @copyright 2011 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
/** @var string help message */
public $_helpbutton = '';
/** @var string|bool stores the result of the last validation: null - undefined, false - no errors, string - error(s) text */
protected $validationerrors = null;
/** @var bool if element has already been validated **/
protected $wasvalidated = false;
/** @var bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */
protected $nonjsbuttonpressed = false;
/** @var bool Message to display in front of the editor (that there exist grades on this rubric being edited) */
protected $regradeconfirmation = false;
/**
* Constructor for rubric editor
*
* @param string $elementName
* @param string $elementLabel
* @param array $attributes
*/
public function __construct($elementName=null, $elementLabel=null, $attributes=null) {
parent::__construct($elementName, $elementLabel, $attributes);
}
/**
* Old syntax of class constructor. Deprecated in PHP7.
*
* @deprecated since Moodle 3.1
*/
public function MoodleQuickForm_rubriceditor($elementName=null, $elementLabel=null, $attributes=null) {
debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
self::__construct($elementName, $elementLabel, $attributes);
}
/**
* get html for help button
*
* @return string html for help button
*/
public function getHelpButton() {
return $this->_helpbutton;
}
/**
* The renderer will take care itself about different display in normal and frozen states
*
* @return string
*/
public function getElementTemplateType() {
return 'default';
}
/**
* Specifies that confirmation about re-grading needs to be added to this rubric editor.
* $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml()
*
* @see gradingform_rubric_controller::update_or_check_rubric()
* @param int $changelevel
*/
public function add_regrade_confirmation($changelevel) {
$this->regradeconfirmation = $changelevel;
}
/**
* Returns html string to display this element
*
* @return string
*/
public function toHtml() {
global $PAGE;
$html = $this->_getTabs();
$renderer = $PAGE->get_renderer('gradingform_rubric');
$data = $this->prepare_data(null, $this->wasvalidated);
if (!$this->_flagFrozen) {
$mode = gradingform_rubric_controller::DISPLAY_EDIT_FULL;
$module = array('name'=>'gradingform_rubriceditor', 'fullpath'=>'/grade/grading/form/rubric/js/rubriceditor.js',
'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'),
array('criterionempty', 'gradingform_rubric'), array('levelempty', 'gradingform_rubric')
));
$PAGE->requires->js_init_call('M.gradingform_rubriceditor.init', array(
array('name' => $this->getName(),
'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
'leveltemplate' => $renderer->level_template($mode, $data['options'], $this->getName())
)),
true, $module);
} else {
// Rubric is frozen, no javascript needed
if ($this->_persistantFreeze) {
$mode = gradingform_rubric_controller::DISPLAY_EDIT_FROZEN;
} else {
$mode = gradingform_rubric_controller::DISPLAY_PREVIEW;
}
}
if ($this->regradeconfirmation) {
if (!isset($data['regrade'])) {
$data['regrade'] = 1;
}
$html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
}
if ($this->validationerrors) {
$html .= html_writer::div($renderer->notification($this->validationerrors));
}
$html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName());
return $html;
}
/**
* Prepares the data passed in $_POST:
* - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled)
* sets $this->nonjsbuttonpressed to true/false if such button was pressed
* - if options not passed (i.e. we create a new rubric) fills the options array with the default values
* - if options are passed completes the options array with unchecked checkboxes
* - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string
* and stores it in $this->validationerrors
*
* @param array $value
* @param boolean $withvalidation whether to enable data validation
* @return array
*/
protected function prepare_data($value = null, $withvalidation = false) {
if (null === $value) {
$value = $this->getValue();
}
if ($this->nonjsbuttonpressed === null) {
$this->nonjsbuttonpressed = false;
}
$totalscore = 0;
$errors = array();
$return = array('criteria' => array(), 'options' => gradingform_rubric_controller::get_default_options());
if (!isset($value['criteria'])) {
$value['criteria'] = array();
$errors['err_nocriteria'] = 1;
}
// If options are present in $value, replace default values with submitted values
if (!empty($value['options'])) {
foreach (array_keys($return['options']) as $option) {
// special treatment for checkboxes
if (!empty($value['options'][$option])) {
$return['options'][$option] = $value['options'][$option];
} else {
$return['options'][$option] = null;
}
}
}
if (is_array($value)) {
// for other array keys of $value no special treatmeant neeeded, copy them to return value as is
foreach (array_keys($value) as $key) {
if ($key != 'options' && $key != 'criteria') {
$return[$key] = $value[$key];
}
}
}
// iterate through criteria
$lastaction = null;
$lastid = null;
$overallminscore = $overallmaxscore = 0;
foreach ($value['criteria'] as $id => $criterion) {
if ($id == 'addcriterion') {
$id = $this->get_next_id(array_keys($value['criteria']));
$criterion = array('description' => '', 'levels' => array());
$i = 0;
// when adding new criterion copy the number of levels and their scores from the last criterion
if (!empty($value['criteria'][$lastid]['levels'])) {
foreach ($value['criteria'][$lastid]['levels'] as $lastlevel) {
$criterion['levels']['NEWID'.($i++)]['score'] = $lastlevel['score'];
}
} else {
$criterion['levels']['NEWID'.($i++)]['score'] = 0;
}
// add more levels so there are at least 3 in the new criterion. Increment by 1 the score for each next one
for ($i=$i; $i<3; $i++) {
$criterion['levels']['NEWID'.$i]['score'] = $criterion['levels']['NEWID'.($i-1)]['score'] + 1;
}
// set other necessary fields (definition) for the levels in the new criterion
foreach (array_keys($criterion['levels']) as $i) {
$criterion['levels'][$i]['definition'] = '';
}
$this->nonjsbuttonpressed = true;
}
$levels = array();
$minscore = $maxscore = null;
if (array_key_exists('levels', $criterion)) {
foreach ($criterion['levels'] as $levelid => $level) {
if ($levelid == 'addlevel') {
$levelid = $this->get_next_id(array_keys($criterion['levels']));
$level = array(
'definition' => '',
'score' => 0,
);
foreach ($criterion['levels'] as $lastlevel) {
if (isset($lastlevel['score'])) {
$level['score'] = max($level['score'], ceil(unformat_float($lastlevel['score'])) + 1);
}
}
$this->nonjsbuttonpressed = true;
}
if (!array_key_exists('delete', $level)) {
$score = unformat_float($level['score'], true);
if ($withvalidation) {
if (!strlen(trim($level['definition']))) {
$errors['err_nodefinition'] = 1;
$level['error_definition'] = true;
}
if ($score === null || $score === false) {
$errors['err_scoreformat'] = 1;
$level['error_score'] = true;
}
}
$levels[$levelid] = $level;
if ($minscore === null || $score < $minscore) {
$minscore = $score;
}
if ($maxscore === null || $score > $maxscore) {
$maxscore = $score;
}
} else {
$this->nonjsbuttonpressed = true;
}
}
}
$totalscore += (float)$maxscore;
$criterion['levels'] = $levels;
if ($withvalidation && !array_key_exists('delete', $criterion)) {
if (count($levels)<2) {
$errors['err_mintwolevels'] = 1;
$criterion['error_levels'] = true;
}
if (!strlen(trim($criterion['description']))) {
$errors['err_nodescription'] = 1;
$criterion['error_description'] = true;
}
$overallmaxscore += $maxscore;
$overallminscore += $minscore;
}
if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
unset($criterion['moveup']);
if ($lastid !== null) {
$lastcriterion = $return['criteria'][$lastid];
unset($return['criteria'][$lastid]);
$return['criteria'][$id] = $criterion;
$return['criteria'][$lastid] = $lastcriterion;
} else {
$return['criteria'][$id] = $criterion;
}
$lastaction = null;
$lastid = $id;
$this->nonjsbuttonpressed = true;
} else if (array_key_exists('delete', $criterion)) {
$this->nonjsbuttonpressed = true;
} else {
if (array_key_exists('movedown', $criterion)) {
unset($criterion['movedown']);
$lastaction = 'movedown';
$this->nonjsbuttonpressed = true;
}
$return['criteria'][$id] = $criterion;
$lastid = $id;
}
}
if ($totalscore <= 0) {
$errors['err_totalscore'] = 1;
}
// add sort order field to criteria
$csortorder = 1;
foreach (array_keys($return['criteria']) as $id) {
$return['criteria'][$id]['sortorder'] = $csortorder++;
}
// create validation error string (if needed)
if ($withvalidation) {
if (!$return['options']['lockzeropoints']) {
if ($overallminscore == $overallmaxscore) {
$errors['err_novariations'] = 1;
}
}
if (count($errors)) {
$rv = array();
foreach ($errors as $error => $v) {
$rv[] = get_string($error, 'gradingform_rubric');
}
$this->validationerrors = join('<br/ >', $rv);
} else {
$this->validationerrors = false;
}
$this->wasvalidated = true;
}
return $return;
}
/**
* Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns
*
* @param array $ids
* @return string
*/
protected function get_next_id($ids) {
$maxid = 0;
foreach ($ids as $id) {
if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) {
$maxid = (int)$matches[1];
}
}
return 'NEWID'.($maxid+1);
}
/**
* Checks if a submit button was pressed which is supposed to be processed on client side by JS
* but user seem to have disabled JS in the browser.
* (buttons 'add criteria', 'add level', 'move up', 'move down', etc.)
* In this case the form containing this element is prevented from being submitted
*
* @param array $value
* @return boolean true if non-submit button was pressed and not processed by JS
*/
public function non_js_button_pressed($value) {
if ($this->nonjsbuttonpressed === null) {
$this->prepare_data($value);
}
return $this->nonjsbuttonpressed;
}
/**
* Validates that rubric has at least one criterion, at least two levels within one criterion,
* each level has a valid score, all levels have filled definitions and all criteria
* have filled descriptions
*
* @param array $value
* @return string|false error text or false if no errors found
*/
public function validate($value) {
if (!$this->wasvalidated) {
$this->prepare_data($value, true);
}
return $this->validationerrors;
}
/**
* Prepares the data for saving
*
* @see prepare_data()
* @param array $submitValues
* @param boolean $assoc
* @return array
*/
public function exportValue(&$submitValues, $assoc = false) {
$value = $this->prepare_data($this->_findValue($submitValues));
return $this->_prepareValue($value, $assoc);
}
}
+282
View File
@@ -0,0 +1,282 @@
/*
.gradingform_rubric.editor[.frozen|.editable]
.criteria
.criterion[.first][.last][.odd|.even]
.controls
.moveup
[input type=submit]
.delete
[input type=submit]
.movedown
[input type=submit]
.description
.levels
td.level[.first][.last][.odd|.even]
div.level-wrapper
.definition
[textarea]
.score
span
[input type=text]
.delete
[input type=submit]
.addlevel
[input type=submit]
.remark
textarea
.addcriterion
[input type=submit]
.options
.optionsheading
.option.OPTIONNAME
.gradingform_rubric[.review][.evaluate[.editable|.frozen]]
.criteria
.criterion[.first][.last][.odd|.even]
.description
.levels
td.level[.first][.last][.odd|.even]
div.level-wrapper
div.radio
input
.definition
.score
span
*/
.gradingform_rubric_editform .status {
font-weight: normal;
text-transform: uppercase;
font-size: 60%;
padding: 0.25em;
border: 1px solid #eee;
}
.gradingform_rubric_editform .status.ready {
background-color: #e7f1c3;
border-color: #aea;
}
.gradingform_rubric_editform .status.draft {
background-color: #f3f2aa;
border-color: #ee2;
}
.gradingform_rubric {
padding-bottom: 1.5em;
}
.gradingform_rubric.editor .criterion .controls,
.gradingform_rubric .criterion .description,
.gradingform_rubric .criterion .levels,
.gradingform_rubric.editor .criterion .addlevel,
.gradingform_rubric .criterion .remark,
.gradingform_rubric .criterion .levels .level {
vertical-align: top;
}
.gradingform_rubric.editor .criterion .controls,
.gradingform_rubric .criterion .description,
.gradingform_rubric.editor .criterion .addlevel,
.gradingform_rubric .criterion .remark,
.gradingform_rubric .criterion .levels .level {
padding: 3px;
}
.gradingform_rubric .criteria {
height: 100%;
display: flex;
width: 100%;
overflow: auto;
}
.gradingform_rubric .criterion {
border: 1px solid #ddd;
overflow: hidden;
}
.gradingform_rubric .criterion.even td {
background: #f0f0f0;
}
.gradingform_rubric .criterion.odd td {
background: white;
}
.gradingform_rubric .criterion .description {
min-width: 150px;
font-weight: bold;
}
.gradingform_rubric .criterion .levels table {
width: 100%;
height: 100%;
}
.gradingform_rubric .criterion .levels,
.gradingform_rubric .criterion .levels table,
.gradingform_rubric .criterion .levels table tbody {
padding: 0;
margin: 0;
}
.gradingform_rubric .criterion .levels .level {
border-left: 1px solid #ddd;
max-width: 150px;
}
.gradingform_rubric .criterion .levels .level .level-wrapper {
position: relative;
}
.gradingform_rubric .criterion .levels .level.last {
border-right: 1px solid #ddd;
}
.gradingform_rubric .plainvalue.empty {
font-style: italic;
color: #aaa;
}
/* Make invisible the buttons 'Move up' for the first criterion and
'Move down' for the last, because those buttons will make no change */
.gradingform_rubric.editor .criterion.first .controls .moveup input,
.gradingform_rubric.editor .criterion.last .controls .movedown input {
display: none;
}
/* replace buttons with images */
.gradingform_rubric.editor .delete input,
.gradingform_rubric.editor .duplicate input,
.gradingform_rubric.editor .moveup input,
.gradingform_rubric.editor .movedown input {
text-indent: -1000em;
cursor: pointer;
border: none;
}
.gradingform_rubric.editor .criterion .controls .delete input {
width: 24px;
height: 24px;
background: transparent url([[pix:t/delete]]) no-repeat center;
margin: .3em .3em 0 .3em;
}
.gradingform_rubric.editor .criterion .controls .duplicate input {
width: 24px;
height: 24px;
background: transparent url([[pix:t/copy]]) no-repeat center;
margin: .3em .3em 0 .3em;
}
.gradingform_rubric.editor .levels .level .delete input {
width: 24px;
height: 24px;
background: transparent url([[pix:t/delete]]) no-repeat center;
}
.gradingform_rubric.editor .moveup input {
width: 24px;
height: 24px;
background: transparent url([[pix:t/up]]) no-repeat center;
margin: .3em .3em 0 .3em;
}
.gradingform_rubric.editor .movedown input {
width: 24px;
height: 24px;
background: transparent url([[pix:t/down]]) no-repeat center;
margin: .3em .3em 0 .3em;
}
.gradingform_rubric.editor .addcriterion input,
.gradingform_rubric.editor .addlevel input {
background: #fff url([[pix:t/add]]) no-repeat 7px 8px;
display: block;
color: #555;
font-weight: bold;
text-decoration: none;
}
.gradingform_rubric.editor .addcriterion input {
height: 30px;
line-height: 29px;
margin-bottom: 14px;
padding-left: 20px;
padding-right: 10px;
}
.gradingform_rubric.editor .addlevel input {
padding-left: 24px;
padding-right: 8px;
}
.gradingform_rubric .options .optionsheading {
font-weight: bold;
font-size: 1.1em;
padding-bottom: 5px;
}
.gradingform_rubric .options .option {
padding-bottom: 2px;
}
.gradingform_rubric .options .option label {
margin-left: 5px;
}
.gradingform_rubric .options .option .value {
margin-left: 5px;
font-weight: bold;
}
.gradingform_rubric .criterion .levels.error {
border: 1px solid red;
}
.gradingform_rubric .criterion .description.error,
.gradingform_rubric .criterion .levels .level .definition.error,
.gradingform_rubric .criterion .levels .level .score.error {
background: #fdd;
}
.gradingform_rubric-regrade {
padding: 10px;
background: #fdd;
border: 1px solid #f00;
margin-bottom: 10px;
}
.gradingform_rubric-restored {
padding: 10px;
background: #ffd;
border: 1px solid #ff0;
margin-bottom: 10px;
}
.gradingform_rubric-error {
color: red;
font-weight: bold;
}
/* special classes for elements created by rubriceditor.js */
.gradingform_rubric.editor .hiddenelement {
display: none;
}
.gradingform_rubric.editor .pseudotablink {
background-color: transparent;
border: 0 solid;
height: 1px;
width: 1px;
color: transparent;
padding: 0;
margin: 0;
position: relative;
float: right;
}
.gradingpanel-gradingform_rubric [aria-checked="true"] {
border: 1px solid black;
}
@@ -0,0 +1,127 @@
{{!
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="mb-3">
<div class="d-flex align-items-center mb-2">
<h5 id="criterion-description-{{id}}" class="px-0 mb-0 description font-weight-bold">{{{description}}}</h5>
<button class="criterion-toggle btn btn-icon icon-no-margin text-reset p-0 font-weight-bold mb-0 ml-auto"
type="button"
data-toggle="collapse"
data-target="#criteria-{{id}}"
aria-expanded="true"
aria-controls="criteria-{{id}}">
<span class="collapsed-icon">
{{#pix}} t/collapsed, core {{/pix}}
<span class="sr-only">{{#str}} expandcriterion, core_grades {{/str}}</span>
</span>
<span class="expanded-icon">
{{#pix}} t/expanded, core {{/pix}}
<span class="sr-only">{{#str}} collapsecriterion, core_grades {{/str}}</span>
</span>
</button>
</div>
<div class="collapse show" id="criteria-{{id}}" role="radiogroup" aria-labelledby="criterion-description-{{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">
<span>
{{{definition}}}
</span>
<span class="pull-right">
{{#str}}pointsvalue, gradingform_rubric, {{score}}{{/str}}
</span>
</label>
</div>
{{/levels}}
<div class="mb-3">
<label class="text-muted" for="advancedgrading-criteria-{{id}}-remark">{{#str}} additionalfeedback, core_grades {{/str}}</label>
<textarea class="form-control"
name="advancedgrading[criteria][{{id}}][remark]"
id="advancedgrading-criteria-{{id}}-remark"
cols="10"
rows="1"
data-max-rows="5"
data-auto-rows="true"
>{{{remark}}}</textarea>
</div>
</div>
</div>
{{/criteria}}
</div>
</form>
{{#js}}
require(['core/auto_rows'], function(AutoRows) {
AutoRows.init(document.getElementById('gradingform_rubric-{{uniqid}}'));
});
{{/js}}
@@ -0,0 +1,517 @@
<?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/>.
/**
* Steps definitions for rubrics.
*
* @package gradingform_rubric
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
use Behat\Gherkin\Node\TableNode;
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Exception\ExpectationException;
/**
* Steps definitions to help with rubrics.
*
* @package gradingform_rubric
* @category test
* @copyright 2013 David Monllaó
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_gradingform_rubric extends behat_base {
/**
* @var The number of levels added by default when a rubric is created.
*/
const DEFAULT_RUBRIC_LEVELS = 3;
/**
* Defines the rubric with the provided data, following rubric's definition grid cells.
*
* This method fills the rubric of the rubric definition
* form; the provided TableNode should contain one row for
* each criterion and each cell of the row should contain:
* # Criterion description
* # Criterion level 1 name
* # Criterion level 1 points
* # Criterion level 2 name
* # Criterion level 2 points
* # Criterion level 3 .....
*
* Works with both JS and non-JS.
*
* @When /^I define the following rubric:$/
* @throws ExpectationException
* @param TableNode $rubric
*/
public function i_define_the_following_rubric(TableNode $rubric) {
// Being a smart method is nothing good when we talk about step definitions, in
// this case we didn't have any other options as there are no labels no elements
// id we can point to without having to "calculate" them.
$steptableinfo = '| criterion description | level1 name | level1 points | level2 name | level2 points | ...';
$criteria = $rubric->getRows();
$addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_rubric'));
// Cleaning the current ones.
$deletebuttons = $this->find_all('css', "input[value='" . get_string('criteriondelete', 'gradingform_rubric') . "']");
if ($deletebuttons) {
// We should reverse the deletebuttons because otherwise once we delete
// the first one the DOM will change and the [X] one will not exist anymore.
$deletebuttons = array_reverse($deletebuttons, true);
foreach ($deletebuttons as $button) {
$this->click_and_confirm($button);
}
}
// The level number (NEWID$N) is not reset after each criterion.
$levelnumber = 1;
// The next criterion is created with the same number of levels than the last criterion.
$defaultnumberoflevels = self::DEFAULT_RUBRIC_LEVELS;
if ($criteria) {
foreach ($criteria as $criterionit => $criterion) {
// Unset empty levels in criterion.
foreach ($criterion as $i => $value) {
if (empty($value)) {
unset($criterion[$i]);
}
}
// Remove empty criterion, as TableNode might contain them to make table rows equal size.
$newcriterion = array();
foreach ($criterion as $k => $c) {
if (!empty($c)) {
$newcriterion[$k] = $c;
}
}
$criterion = $newcriterion;
// Checking the number of cells.
if (count($criterion) % 2 === 0) {
throw new ExpectationException(
'The criterion levels should contain both definition and points, follow this format:' . $steptableinfo,
$this->getSession()
);
}
// Minimum 2 levels per criterion.
// description + definition1 + score1 + definition2 + score2 = 5.
if (count($criterion) < 5) {
throw new ExpectationException(
get_string('err_mintwolevels', 'gradingform_rubric'),
$this->getSession()
);
}
// Add new criterion.
$this->execute('behat_general::i_click_on', [
$addcriterionbutton,
'NodeElement',
]);
$criterionroot = 'rubric[criteria][NEWID' . ($criterionit + 1) . ']';
// Getting the criterion description, this one is visible by default.
$this->set_rubric_field_value($criterionroot . '[description]', array_shift($criterion), true);
// When JS is disabled each criterion's levels name numbers starts from 0.
if (!$this->running_javascript()) {
$levelnumber = 0;
}
// Setting the correct number of levels.
$nlevels = count($criterion) / 2;
if ($nlevels < $defaultnumberoflevels) {
// Removing levels if there are too much levels.
// When we add a new level the NEWID$N is increased from the last criterion.
$lastcriteriondefaultlevel = $defaultnumberoflevels + $levelnumber - 1;
$lastcriterionlevel = $nlevels + $levelnumber - 1;
for ($i = $lastcriteriondefaultlevel; $i > $lastcriterionlevel; $i--) {
// If JS is disabled seems that new levels are not added.
if ($this->running_javascript()) {
$deletelevel = $this->find_button($criterionroot . '[levels][NEWID' . $i . '][delete]');
$this->click_and_confirm($deletelevel);
} else {
// Only if the level exists.
$buttonname = $criterionroot . '[levels][NEWID' . $i . '][delete]';
if ($deletelevel = $this->getSession()->getPage()->findButton($buttonname)) {
$this->execute('behat_general::i_click_on', [
$deletelevel,
'NodeElement',
]);
}
}
}
} else if ($nlevels > $defaultnumberoflevels) {
// Adding levels if we don't have enough.
$addlevel = $this->find_button($criterionroot . '[levels][addlevel]');
for ($i = ($defaultnumberoflevels + 1); $i <= $nlevels; $i++) {
$this->execute('behat_general::i_click_on', [
$addlevel,
'NodeElement',
]);
}
}
// Updating it.
if ($nlevels > self::DEFAULT_RUBRIC_LEVELS) {
$defaultnumberoflevels = $nlevels;
} else {
// If it is less than the default value it sets it to
// the default value.
$defaultnumberoflevels = self::DEFAULT_RUBRIC_LEVELS;
}
foreach ($criterion as $i => $value) {
$levelroot = $criterionroot . '[levels][NEWID' . $levelnumber . ']';
if ($i % 2 === 0) {
// Pairs are the definitions.
$fieldname = $levelroot . '[definition]';
$this->set_rubric_field_value($fieldname, $value);
} else {
// Odds are the points.
// Checking it now, we would need to remove it if we are testing the form validations...
if (!is_numeric($value)) {
throw new ExpectationException(
'The points cells should contain numeric values, follow this format: ' . $steptableinfo,
$this->getSession()
);
}
$fieldname = $levelroot . '[score]';
$this->set_rubric_field_value($fieldname, $value, true);
// Increase the level by one every 2 cells.
$levelnumber++;
}
}
}
}
}
/**
* Replaces a value from the specified criterion. You can use it when editing rubrics, to set both name or points.
*
* @When /^I replace "(?P<current_value_string>(?:[^"]|\\")*)" rubric level with "(?P<value_string>(?:[^"]|\\")*)" in "(?P<criterion_string>(?:[^"]|\\")*)" criterion$/
* @throws ElementNotFoundException
* @param string $currentvalue
* @param string $value
* @param string $criterionname
*/
public function i_replace_rubric_level_with($currentvalue, $value, $criterionname) {
$currentvalueliteral = behat_context_helper::escape($currentvalue);
$criterionliteral = behat_context_helper::escape($criterionname);
$criterionxpath = "//div[@id='rubric-rubric']" .
"/descendant::td[contains(concat(' ', normalize-space(@class), ' '), ' description ')]";
// It differs between JS on/off.
if ($this->running_javascript()) {
$criterionxpath .= "/descendant::span[@class='textvalue'][text()=$criterionliteral]" .
"/ancestor::tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]";
} else {
$criterionxpath .= "/descendant::textarea[text()=$criterionliteral]" .
"/ancestor::tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]";
}
$inputxpath = $criterionxpath .
"/descendant::input[@type='text'][@value=$currentvalueliteral]";
$textareaxpath = $criterionxpath .
"/descendant::textarea[text()=$currentvalueliteral]";
if ($this->running_javascript()) {
$spansufix = "/ancestor::div[@class='level-wrapper']" .
"/descendant::div[@class='definition']" .
"/descendant::span[@class='textvalue']";
// Expanding the level input boxes.
$this->execute('behat_general::i_click_on', [
$inputxpath . $spansufix . '|' . $textareaxpath . $spansufix,
'xpath',
]);
$this->execute(
'behat_forms::i_set_the_field_with_xpath_to',
[
$inputxpath . '|' . $textareaxpath,
$value,
]
);
} else {
$fieldnode = $this->find('xpath', $inputxpath . '|' . $textareaxpath);
$this->set_rubric_field_value($fieldnode->getAttribute('name'), $value);
}
}
/**
* Grades filling the current page rubric. Set one line per criterion and for each criterion set "| Criterion name | Points | Remark |".
*
* @When /^I grade by filling the rubric with:$/
*
* @throws ExpectationException
* @param TableNode $rubric
*/
public function i_grade_by_filling_the_rubric_with(TableNode $rubric) {
$criteria = $rubric->getRowsHash();
$stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' .
' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |';
// If running Javascript, ensure we zoom in before filling the grades.
if ($this->running_javascript()) {
$this->execute('behat_general::click_link', get_string('togglezoom', 'mod_assign'));
}
// First element -> name, second -> points, third -> Remark.
foreach ($criteria as $name => $criterion) {
// We only expect the points and the remark, as the criterion name is $name.
if (count($criterion) !== 2) {
throw new ExpectationException($stepusage, $this->getSession());
}
// Numeric value here.
$points = $criterion[0];
if (!is_numeric($points)) {
throw new ExpectationException($stepusage, $this->getSession());
}
// Selecting a value.
// When JS is disabled there are radio options, with JS enabled divs.
$selectedlevelxpath = $this->get_level_xpath($points);
if ($this->running_javascript()) {
// Only clicking on the selected level if it was not already selected.
$levelnode = $this->find('xpath', $selectedlevelxpath);
// Using in_array() as there are only a few elements.
if (!$levelnode->hasClass('checked')) {
$levelnodexpath = $selectedlevelxpath . "//div[contains(concat(' ', normalize-space(@class), ' '), ' score ')]";
$this->execute('behat_general::i_click_on_in_the',
array($levelnodexpath, "xpath_element", $this->escape($name), "table_row")
);
}
} else {
// Getting the name of the field.
$radioxpath = $this->get_criterion_xpath($name) .
$selectedlevelxpath . "/descendant::input[@type='radio']";
$radionode = $this->find('xpath', $radioxpath);
// which will delegate the process to the field type.
$radionode->setValue($radionode->getAttribute('value'));
}
// Setting the remark.
// First we need to get the textarea name, then we can set the value.
$textarea = $this->get_node_in_container('css_element', 'textarea', 'table_row', $name);
$this->execute('behat_forms::i_set_the_field_to', array($textarea->getAttribute('name'), $criterion[1]));
}
// If running Javascript, then ensure to close zoomed rubric.
if ($this->running_javascript()) {
$this->execute('behat_general::click_link', get_string('togglezoom', 'mod_assign'));
}
}
/**
* Checks that the level was previously selected and the user changed to another level.
*
* @Then /^the level with "(?P<points_number>\d+)" points was previously selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @param string $criterionname
* @param int $points
* @return void
*/
public function the_level_with_points_was_previously_selected_for_the_rubric_criterion($points, $criterionname) {
$levelxpath = $this->get_criterion_xpath($criterionname) .
$this->get_level_xpath($points) .
"[contains(concat(' ', normalize-space(@class), ' '), ' currentchecked ')]";
// Works both for JS and non-JS.
// - JS: Class -> checked is there when is marked as green.
// - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
// grade @class contains checked.
$levelxpath .= "[not(contains(concat(' ', normalize-space(@class), ' '), ' checked '))]" .
"[not(/descendant::input[@type='radio'][@checked!='checked'])]";
try {
$this->find('xpath', $levelxpath);
} catch (ElementNotFoundException $e) {
throw new ExpectationException('"' . $points . '" points level was not previously selected', $this->getSession());
}
}
/**
* Checks that the level is currently selected. Works both when grading rubrics and viewing graded rubrics.
*
* @Then /^the level with "(?P<points_number>\d+)" points is selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @param string $criterionname
* @param int $points
* @return void
*/
public function the_level_with_points_is_selected_for_the_rubric_criterion($points, $criterionname) {
$levelxpath = $this->get_criterion_xpath($criterionname) .
$this->get_level_xpath($points);
// Works both for JS and non-JS.
// - JS: Class -> checked is there when is marked as green.
// - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
// grade @class contains checked.
$levelxpath .= "[" .
"contains(concat(' ', normalize-space(@class), ' '), ' checked ')" .
" or " .
"/descendant::input[@type='radio'][@checked='checked']" .
"]";
try {
$this->find('xpath', $levelxpath);
} catch (ElementNotFoundException $e) {
throw new ExpectationException('"' . $points . '" points level is not selected', $this->getSession());
}
}
/**
* Checks that the level is not currently selected. Works both when grading rubrics and viewing graded rubrics.
*
* @Then /^the level with "(?P<points_number>\d+)" points is not selected for the rubric criterion "(?P<criterion_name_string>(?:[^"]|\\")*)"$/
* @throws ExpectationException
* @param string $criterionname
* @param int $points
* @return void
*/
public function the_level_with_points_is_not_selected_for_the_rubric_criterion($points, $criterionname) {
$levelxpath = $this->get_criterion_xpath($criterionname) .
$this->get_level_xpath($points);
// Works both for JS and non-JS.
// - JS: Class -> checked is there when is marked as green.
// - Non-JS: When editing a rubric definition, there are radio inputs and when viewing a
// grade @class contains checked.
$levelxpath .= "[not(contains(concat(' ', normalize-space(@class), ' '), ' checked '))]" .
"[./descendant::input[@type='radio'][@checked!='checked'] or not(./descendant::input[@type='radio'])]";
try {
$this->find('xpath', $levelxpath);
} catch (ElementNotFoundException $e) {
throw new ExpectationException('"' . $points . '" points level is selected', $this->getSession());
}
}
/**
* Makes a hidden rubric field visible (if necessary) and sets a value on it.
*
* @param string $name The name of the field
* @param string $value The value to set
* @param bool $visible
* @return void
*/
protected function set_rubric_field_value($name, $value, $visible = false) {
// Fields are hidden by default.
if ($this->running_javascript() == true && $visible === false) {
$xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]";
$this->execute('behat_general::i_click_on', [
$xpath,
'xpath',
]);
}
// Set the value now.
$this->execute(
'behat_forms::i_set_the_field_to',
[
$name,
$value,
]
);
}
/**
* Performs click confirming the action.
*
* @param NodeElement $node
* @return void
*/
protected function click_and_confirm($node) {
// Clicks to perform the action.
$this->execute('behat_general::i_click_on', [
$node,
'NodeElement',
]);
// Confirms the delete.
if ($this->running_javascript()) {
$this->execute('behat_general::i_click_on_in_the', [
get_string('yes'),
'button',
get_string('confirmation', 'admin'),
'dialogue',
]);
}
}
/**
* Returns the xpath representing a selected level.
*
* It is not including the path to the criterion.
*
* It is the xpath when grading a rubric or viewing a rubric,
* it is not the same xpath when editing a rubric.
*
* @param int $points
* @return string
*/
protected function get_level_xpath($points) {
return "//td[contains(concat(' ', normalize-space(@class), ' '), ' level ')]" .
"[./descendant::span[@class='scorevalue'][text()='$points']]";
}
/**
* Returns the xpath representing the selected criterion.
*
* It is the xpath when grading a rubric or viewing a rubric,
* it is not the same xpath when editing a rubric.
*
* @param string $criterionname Literal including the criterion name.
* @return string
*/
protected function get_criterion_xpath($criterionname) {
$literal = behat_context_helper::escape($criterionname);
return "//tr[contains(concat(' ', normalize-space(@class), ' '), ' criterion ')]" .
"[./descendant::td[@class='description'][text()=$literal]]";
}
}
@@ -0,0 +1,147 @@
@gradingform @gradingform_rubric
Feature: Rubrics can be created and edited
In order to use and refine rubrics to grade students
As a teacher
I need to edit previously used rubrics
@javascript
Scenario: I can use rubrics to grade and edit them later updating students grades
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "activity" exists:
| activity | assign |
| course | C1 |
| section | 1 |
| name | Test assignment 1 name |
| intro | Test assignment description |
| assignfeedback_comments_enabled | 1 |
| assignfeedback_editpdf_enabled | 1 |
| advancedgradingmethod_submissions | rubric |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
When I go to "Test assignment 1 name" advanced grading definition page
# Defining a rubric.
And I set the following fields to these values:
| Name | Assignment 1 rubric |
| Description | Rubric test description |
And I define the following rubric:
| TMP Criterion 1 | TMP Level 11 | 11 | TMP Level 12 | 12 |
| TMP Criterion 2 | TMP Level 21 | 21 | TMP Level 22 | 22 |
| TMP Criterion 3 | TMP Level 31 | 31 | TMP Level 32 | 32 |
| TMP Criterion 4 | TMP Level 41 | 41 | TMP Level 42 | 42 |
# Checking that only the last ones are saved.
And I define the following rubric:
| Criterion 1 | Level 11 | 1 | Level 12 | 20 | Level 13 | 40 | Level 14 | 50 |
| Criterion 2 | Level 21 | 10 | Level 22 | 20 | Level 23 | 30 | | |
| Criterion 3 | Level 31 | 5 | Level 32 | 20 | | | | |
And I press "Save as draft"
And I go to "Test assignment 1 name" advanced grading definition page
And I click on "Move down" "button" in the "Criterion 1" "table_row"
And I press "Save rubric and make it ready"
Then I should see "Ready for use"
# Grading two students.
And I navigate to "Assignment" in current page administration
And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
And I grade by filling the rubric with:
| Criterion 1 | 50 | Very good |
And I press "Save changes"
# Checking that it complains if you don't select a level for each criterion.
And I should see "Please choose something for each criterion"
And I grade by filling the rubric with:
| Criterion 1 | 50 | Very good |
| Criterion 2 | 10 | Mmmm, you can do it better |
| Criterion 3 | 5 | Not good |
And I complete the advanced grading form with these values:
| Feedback comments | In general... work harder... |
# Checking that the user grade is correct.
And I should see "65" in the "Student 1" "table_row"
# Updating the user grade.
And I am on the "Test assignment 1 name" "assign activity" page
And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
And I grade by filling the rubric with:
| Criterion 1 | 20 | Bad, I changed my mind |
| Criterion 2 | 10 | Mmmm, you can do it better |
| Criterion 3 | 5 | Not good |
#And the level with "50" points was previously selected for the rubric criterion "Criterion 1"
#And the level with "20" points is selected for the rubric criterion "Criterion 1"
And I save the advanced grading form
And I should see "35" in the "Student 1" "table_row"
And I log out
# Viewing it as a student.
And I am on the "Test assignment 1 name" "assign activity" page logged in as student1
And I should see "35" in the ".feedback" "css_element"
And I should see "Rubric test description" in the ".feedback" "css_element"
And I should see "In general... work harder..."
And the level with "10" points is selected for the rubric criterion "Criterion 2"
And the level with "20" points is selected for the rubric criterion "Criterion 1"
And the level with "5" points is selected for the rubric criterion "Criterion 3"
And I log out
And I am on the "Course 1" course page logged in as teacher1
# Editing a rubric definition without regrading students.
And I go to "Test assignment 1 name" advanced grading definition page
And "Save as draft" "button" should not exist
And I click on "Move up" "button" in the "Criterion 1" "table_row"
And I replace "Level 11" rubric level with "Level 11 edited" in "Criterion 1" criterion
And I press "Save"
And I should see "You are about to save changes to a rubric that has already been used for grading."
And I set the field "menurubricregrade" to "Do not mark for regrade"
And I press "Continue"
And I log out
# Check that the student still sees the grade.
And I am on the "Test assignment 1 name" "assign activity" page logged in as student1
And I should see "35" in the ".feedback" "css_element"
And the level with "20" points is selected for the rubric criterion "Criterion 1"
And I log out
# Editing a rubric with significant changes.
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I go to "Test assignment 1 name" advanced grading definition page
And I click on "Move down" "button" in the "Criterion 2" "table_row"
And I replace "1" rubric level with "60" in "Criterion 1" criterion
And I press "Save"
And I should see "You are about to save significant changes to a rubric that has already been used for grading. The gradebook value will be unchanged, but the rubric will be hidden from students until their item is regraded."
And I press "Continue"
And I log out
# Check that the student doesn't see the grade.
And I am on the "Test assignment 1 name" "assign activity" page logged in as student1
And I should see "35" in the ".feedback" "css_element"
And the level with "20" points is not selected for the rubric criterion "Criterion 1"
And I log out
# Regrade student.
And I am on the "Test assignment 1 name" "assign activity" page logged in as teacher1
And I go to "Student 1" "Test assignment 1 name" activity advanced grading page
And I should see "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."
And I save the advanced grading form
And I log out
# Check that the student sees the grade again.
And I am on the "Test assignment 1 name" "assign activity" page logged in as student1
And I should see "31.82" in the ".feedback" "css_element"
And the level with "20" points is not selected for the rubric criterion "Criterion 1"
# Hide all rubric info for students
And I log out
And I am on the "Course 1" course page logged in as teacher1
And I go to "Test assignment 1 name" advanced grading definition page
And I set the field "Allow users to preview rubric (otherwise it will only be displayed after grading)" to ""
And I set the field "Display rubric description during evaluation" to ""
And I set the field "Display rubric description to those being graded" to ""
And I set the field "Display points for each level during evaluation" to ""
And I set the field "Display points for each level to those being graded" to ""
And I press "Save"
And I set the field "menurubricregrade" to "Do not mark for regrade"
And I press "Continue"
And I log out
# Students should not see anything.
And I am on the "Test assignment 1 name" "assign activity" page logged in as student1
And I should not see "Criterion 1" in the ".submissionstatustable" "css_element"
And I should not see "Criterion 2" in the ".submissionstatustable" "css_element"
And I should not see "Criterion 3" in the ".submissionstatustable" "css_element"
And I should not see "Rubric test description" in the ".feedback" "css_element"
@@ -0,0 +1,57 @@
@gradingform @gradingform_rubric @javascript
Feature: Converting rubric score to grades
In order to use and refine rubrics to grade students
As a teacher
I need to be able to use different grade settings
Scenario Outline: Convert rubric scores to grades.
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "scales" exist:
| name | scale |
| Test scale 1 | Disappointing, Good, Very good, Excellent |
And the following "activities" exist:
| activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions |
| assign | Test assignment 1 | Test | C1 | assign1 | <grade> | rubric |
When I log in as "teacher1"
And I change window size to "large"
And I am on "Course 1" course homepage with editing mode on
And I go to "Test assignment 1" advanced grading definition page
And I set the following fields to these values:
| Name | Assignment 1 rubric |
| Description | Rubric test description |
| Calculate grade having a minimum score of the minimum achievable grade for the rubric | <lockzeropoints> |
And I define the following rubric:
| Criterion 1 | Level 11 | 20 | Level 12 | 25 | Level 13 | 40 | Level 14 | 50 |
| Criterion 2 | Level 21 | 20 | Level 22 | 25 | Level 23 | 30 | | |
| Criterion 3 | Level 31 | 10 | Level 32 | 20 | | | | |
And I press "Save rubric and make it ready"
And I navigate to "Assignment" in current page administration
# Grading a student.
And I go to "Student 1" "Test assignment 1" activity advanced grading page
And I grade by filling the rubric with:
| Criterion 1 | 25 | |
| Criterion 2 | 20 | |
| Criterion 3 | 10 | |
And I save the advanced grading form
# Checking that the user grade is correct.
And I should see "<studentgrade>" in the "student1@example.com" "table_row"
And I log out
Examples:
| grade | lockzeropoints | studentgrade |
| 100 | 1 | 55.00 |
| 70 | 1 | 38.50 |
| Test scale 1 | 1 | Good |
| 100 | | 10.00 |
| 70 | | 7.00 |
| Test scale 1 | | Disappointing |
@@ -0,0 +1,68 @@
@gradingform @gradingform_rubric @javascript
Feature: Rubrics can have levels with negative scores
In order to use and refine rubrics to grade students
As a teacher
I need to be able to penalise for very wrong submissions
Scenario: Using negative levels in rubrics
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
| student2 | Student | 2 | student2@example.com |
| student3 | Student | 3 | student3@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
And the following "scales" exist:
| name | scale |
| Test scale 1 | Disappointing, Good, Very good, Excellent |
And the following "activities" exist:
| activity | name | intro | course | idnumber | grade | advancedgradingmethod_submissions |
| assign | Test assignment 1 | Test | C1 | assign1 | 100 | rubric |
And I change window size to "large"
When I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I go to "Test assignment 1" advanced grading definition page
And I set the following fields to these values:
| Name | Assignment 1 rubric |
| Description | Rubric test description |
And I define the following rubric:
| Criterion 1 | Did not try | -11 | Level 12 | 25 | Level 13 | 40 | Level 14 | 50 |
| Criterion 2 | Very bad | -20 | Level 22 | 25 | Level 23 | 30 | | |
| Criterion 3 | Level 31 | 10 | Level 32 | 20 | | | | |
And I press "Save rubric and make it ready"
# Grading a student.
And I navigate to "Assignment" in current page administration
And I go to "Student 1" "Test assignment 1" activity advanced grading page
And I grade by filling the rubric with:
| Criterion 1 | 25 | |
| Criterion 2 | 30 | |
| Criterion 3 | 10 | |
And I save the advanced grading form
And I am on the "Test assignment 1" "assign activity" page
And I go to "Student 2" "Test assignment 1" activity advanced grading page
And I grade by filling the rubric with:
| Criterion 1 | 25 | |
| Criterion 2 | -20 | |
| Criterion 3 | 10 | |
And I save the advanced grading form
And I am on the "Test assignment 1" "assign activity" page
And I go to "Student 3" "Test assignment 1" activity advanced grading page
And I grade by filling the rubric with:
| Criterion 1 | -11 | |
| Criterion 2 | -20 | |
| Criterion 3 | 10 | |
And I save the advanced grading form
# Checking that the user grade is correct.
And I should see "65.00" in the "student1@example.com" "table_row"
And I should see "15.00" in the "student2@example.com" "table_row"
And I should see "0.00" in the "student3@example.com" "table_row"
And I should not see "-" in the "student3@example.com" "table_row"
And I log out
@@ -0,0 +1,56 @@
@gradingform @gradingform_rubric
Feature: Publish rubrics as templates
In order to save time to teachers
As a manager
I need to publish rubrics and make them available to all teachers
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| manager1 | Manager | 1 | manager1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "activities" exist:
| activity | course | idnumber | name | intro | advancedgradingmethod_submissions |
| assign | C1 | A1 | Test assignment 1 name | TA1 | rubric |
| assign | C1 | A2 | Test assignment 2 name | TA2 | rubric |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "system role assigns" exist:
| user | role | contextlevel | reference |
| manager1 | manager | System | |
And I log in as "manager1"
And I am on "Course 1" course homepage
And I go to "Test assignment 1 name" advanced grading definition page
And I set the following fields to these values:
| Name | Assignment 1 rubric |
| Description | Assignment 1 description |
And I define the following rubric:
| Criterion 1 | Level 11 | 11 | Level 12 | 12 |
| Criterion 2 | Level 21 | 21 | Level 22 | 22 |
And I press "Save rubric and make it ready"
When I publish "Test assignment 1 name" grading form definition as a public template
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I set "Test assignment 2 name" activity to use "Assignment 1 rubric" grading form
Then I should see "Advanced grading"
And I should see "Criterion 1"
And I should see "Assignment 1 description"
And I go to "Test assignment 2 name" advanced grading definition page
And I should see "Current rubric status"
@javascript
Scenario: Create a rubric template and reuse it as a teacher, with Javascript enabled
Then the field "Description" matches value "Assignment 1 description"
And I should see "Criterion 1"
And I press "Cancel"
Scenario: Create a rubric template and reuse it as a teacher, with Javascript disabled
Then the field "Description" matches value "Assignment 1 description"
# Trying to avoid pointing by id or name as the code internals may change.
And "//table[@class='criteria']//textarea[text()='Criterion 1']" "xpath_element" should exist
And I press "Cancel"
@@ -0,0 +1,52 @@
@gradingform @gradingform_rubric
Feature: Reuse my rubrics in other activities
In order to save time creating duplicated grading forms
As a teacher
I need to reuse rubrics that I created previously
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | section | idnumber |
| assign | Test assignment 1 name | Test assignment 1 description | C1 | 1 | assign1 |
| assign | Test assignment 2 name | Test assignment 2 description | C1 | 1 | assign1 |
And I am on the "Test assignment 1 name" "assign activity editing" page logged in as teacher1
And I set the following fields to these values:
| Grading method | Rubric |
And I press "Save and return to course"
And I go to "Test assignment 1 name" advanced grading definition page
And I set the following fields to these values:
| Name | Assignment 1 rubric |
| Description | Assignment 1 description |
And I define the following rubric:
| Criterion 1 | Level 11 | 11 | Level 12 | 12 | Level 3 | 13 |
| Criterion 2 | Level 21 | 21 | Level 22 | 22 | Level 3 | 23 |
| Criterion 3 | Level 31 | 31 | Level 32 | 32 | | |
And I press "Save rubric and make it ready"
And I am on the "Test assignment 2 name" "assign activity editing" page
And I set the following fields to these values:
| Grading method | Rubric |
And I press "Save and return to course"
And I set "Test assignment 2 name" activity to use "Assignment 1 rubric" grading form
Then I should see "Ready for use"
And I should see "Criterion 1"
And I should see "Criterion 2"
And I should see "Criterion 3"
And I am on "Course 1" course homepage
And I go to "Test assignment 1 name" advanced grading definition page
And I should see "Criterion 1"
And I should see "Criterion 2"
And I should see "Criterion 3"
@javascript
Scenario: A teacher can reuse one of his/her previously created rubrics, with Javascript enabled
Scenario: A teacher can reuse one of his/her previously created rubrics, with Javascript disabled
@@ -0,0 +1,121 @@
<?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/>.
/**
* Generator for the gradingforum_rubric plugin.
*
* @package gradingform_rubric
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tests\gradingform_rubric\generator;
/**
* Convenience class to create rubric criterion.
*
* @package gradingform_rubric
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class criterion {
/** @var string $description A description of the criterion. */
public $description;
/** @var integer $sortorder sort order of the criterion. */
public $sortorder = 0;
/** @var array $levels The levels for this criterion. */
public $levels = [];
/**
* Constructor for this test_criterion object
*
* @param string $description A description of this criterion.
* @param array $levels
*/
public function __construct(string $description, array $levels = []) {
$this->description = $description;
foreach ($levels as $definition => $score) {
$this->add_level($definition, $score);
}
}
/**
* Adds levels to the criterion.
*
* @param string $definition The definition for this level.
* @param int $score The score received if this level is selected.
* @return self
*/
public function add_level(string $definition, int $score): self {
$this->levels[] = [
'definition' => $definition,
'score' => $score
];
return $this;
}
/**
* Get the description for this criterion.
*
* @return string
*/
public function get_description(): string {
return $this->description;
}
/**
* Get the levels for this criterion.
*
* @return array
*/
public function get_levels(): array {
return $this->levels;
}
/**
* Get all values in an array for use when creating a new guide.
*
* @param int $sortorder
* @return array
*/
public function get_all_values(int $sortorder): array {
return [
'sortorder' => $sortorder,
'description' => $this->get_description(),
'levels' => $this->get_all_level_values(),
];
}
/**
* Get all level values.
*
* @return array
*/
public function get_all_level_values(): array {
$result = [];
foreach ($this->get_levels() as $index => $level) {
$id = $index + 1;
$result["NEWID{$id}"] = $level;
}
return $result;
}
}
@@ -0,0 +1,240 @@
<?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/>.
/**
* Generator for the gradingforum_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();
require_once(__DIR__ . '/rubric.php');
require_once(__DIR__ . '/criterion.php');
use tests\gradingform_rubric\generator\rubric;
use tests\gradingform_rubric\generator\criterion;
/**
* Generator for the gradingforum_rubric plugintype.
*
* @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
*/
class gradingform_rubric_generator extends component_generator_base {
/**
* Create an instance of a rubric.
*
* @param context $context
* @param string $component
* @param string $area
* @param string $name
* @param string $description
* @param array $criteria The list of criteria to add to the generated rubric
* @return gradingform_rubric_controller
*/
public function create_instance(
context $context,
string $component,
string $area,
string $name,
string $description,
array $criteria
): gradingform_rubric_controller {
global $USER;
if ($USER->id === 0) {
throw new \coding_exception('Creation of a rubric must currently be run as a user.');
}
// Fetch the controller for this context/component/area.
$generator = \testing_util::get_data_generator();
$gradinggenerator = $generator->get_plugin_generator('core_grading');
$controller = $gradinggenerator->create_instance($context, $component, $area, 'rubric');
// Generate a definition for the supplied rubric.
$rubric = $this->get_rubric($name, $description);
foreach ($criteria as $name => $criterion) {
$rubric->add_criteria($this->get_criterion($name, $criterion));
}
// Update the controller wih the rubric definition.
$controller->update_definition($rubric->get_definition());
return $controller;
}
/**
* Get a new rubric for use with the rubric controller.
*
* Note: This is just a helper class used to build a new definition. It does not persist the data.
*
* @param string $name
* @param string $description
* @return rubric
*/
protected function get_rubric(string $name, string $description): rubric {
return new rubric($name, $description);
}
/**
* Get a new rubric for use with a gradingform_rubric_generator_rubric.
*
* Note: This is just a helper class used to build a new definition. It does not persist the data.
*
* @param string $description
* @param array $levels Set of levels in the form definition => score
* @return gradingform_rubric_generator_criterion
*/
protected function get_criterion(string $description, array $levels = []): criterion {
return new criterion($description, $levels);
}
/**
* Given a controller instance, fetch the level and criterion information for the specified values.
*
* @param gradingform_controller $controller
* @param string $description The description to match the criterion on
* @param float $score The value to match the level on
* @return array
*/
public function get_level_and_criterion_for_values(
gradingform_controller $controller,
string $description,
float $score
): array {
$definition = $controller->get_definition();
$criteria = $definition->rubric_criteria;
$criterion = $level = null;
$criterion = array_reduce($criteria, function($carry, $criterion) use ($description) {
if ($criterion['description'] === $description) {
$carry = $criterion;
}
return $carry;
}, null);
if ($criterion) {
$criterion = (object) $criterion;
$level = array_reduce($criterion->levels, function($carry, $level) use ($score) {
if ($level['score'] == $score) {
$carry = $level;
}
return $carry;
});
$level = $level ? (object) $level : null;
}
return [
'criterion' => $criterion,
'level' => $level,
];
}
/**
* Get submitted form data for the supplied controller, itemid, and values.
* The returned data is in the format used by rubric when handling form submission.
*
* @param gradingform_rubric_controller $controller
* @param int $itemid
* @param array $values A set of array values where the array key is the name of the criterion, and the value is an
* array with the desired score, and any remark.
*/
public function get_submitted_form_data(gradingform_rubric_controller $controller, int $itemid, array $values): array {
$result = [
'itemid' => $itemid,
'criteria' => [],
];
foreach ($values as $criterionname => ['score' => $score, 'remark' => $remark]) {
[
'criterion' => $criterion,
'level' => $level,
] = $this->get_level_and_criterion_for_values($controller, $criterionname, $score);
$result['criteria'][$criterion->id] = [
'levelid' => $level->id,
'remark' => $remark,
];
}
return $result;
}
/**
* Generate a rubric controller with sample data required for testing of this class.
*
* @param context $context
* @param string $component
* @param string $area
* @return gradingform_rubric_controller
*/
public function get_test_rubric(context $context, string $component, string $area): gradingform_rubric_controller {
$criteria = [
'Spelling is important' => [
'Nothing but mistakes' => 0,
'Several mistakes' => 1,
'No mistakes' => 2,
],
'Pictures' => [
'No pictures' => 0,
'One picture' => 1,
'More than one picture' => 2,
],
];
return $this->create_instance($context, $component, $area, 'testrubric', 'Description text', $criteria);
}
/**
* Fetch a set of sample data.
*
* @param gradingform_rubric_controller $controller
* @param int $itemid
* @param float $spellingscore
* @param string $spellingremark
* @param float $picturescore
* @param string $pictureremark
* @return array
*/
public function get_test_form_data(
gradingform_rubric_controller $controller,
int $itemid,
float $spellingscore,
string $spellingremark,
float $picturescore,
string $pictureremark
): array {
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
return $rubricgenerator->get_submitted_form_data($controller, $itemid, [
'Spelling is important' => [
'score' => $spellingscore,
'remark' => $spellingremark,
],
'Pictures' => [
'score' => $picturescore,
'remark' => $pictureremark,
],
]);
}
}
@@ -0,0 +1,133 @@
<?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/>.
/**
* Generator for the gradingforum_rubric plugin.
*
* @package gradingform_rubric
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tests\gradingform_rubric\generator;
use gradingform_controller;
use stdClass;
/**
* Test rubric.
*
* @package gradingform_rubric
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class rubric {
/** @var array $criteria The criteria for this rubric. */
protected $criteria = [];
/** @var string The name of this rubric. */
protected $name;
/** @var string A description for this rubric. */
protected $description;
/** @var array The rubric options. */
protected $options = [];
/**
* Create a new gradingform_rubric_generator_rubric.
*
* @param string $name
* @param string $description
*/
public function __construct(string $name, string $description) {
$this->name = $name;
$this->description = $description;
$this->set_option('sortlevelsasc', 1);
$this->set_option('lockzeropoints', 1);
$this->set_option('showdescriptionteacher', 1);
$this->set_option('showdescriptionstudent', 1);
$this->set_option('showscoreteacher', 1);
$this->set_option('showscorestudent', 1);
$this->set_option('enableremarks', 1);
$this->set_option('showremarksstudent', 1);
}
/**
* Creates the rubric using the appropriate APIs.
*/
public function get_definition(): stdClass {
return (object) [
'name' => $this->name,
'description_editor' => [
'text' => $this->description,
'format' => FORMAT_HTML,
'itemid' => 1
],
'rubric' => [
'criteria' => $this->get_all_criterion_values(),
'options' => $this->options,
],
'saverubric' => 'Save rubric and make it ready',
'status' => gradingform_controller::DEFINITION_STATUS_READY,
];
}
/**
* Set an option for the rubric.
*
* @param string $key
* @param mixed $value
* @return self
*/
public function set_option(string $key, $value): self {
$this->options[$key] = $value;
return $this;
}
/**
* Adds a criterion to the rubric.
*
* @param criterion $criterion The criterion object (class below).
* @return self
*/
public function add_criteria(criterion $criterion): self {
$this->criteria[] = $criterion;
return $this;
}
/**
* Get all criterion values.
*
* @return array
*/
protected function get_all_criterion_values(): array {
$result = [];
foreach ($this->criteria as $index => $criterion) {
$id = $index + 1;
$result["NEWID{$id}"] = $criterion->get_all_values($id);
}
return $result;
}
}
@@ -0,0 +1,319 @@
<?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/>.
/**
* Generator testcase for the gradingforum_rubric generator.
*
* @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
*/
namespace gradingform_rubric;
use advanced_testcase;
use context_module;
use gradingform_rubric_controller;
use gradingform_controller;
/**
* Generator testcase for the gradingforum_rubric generator.
*
* @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
*/
class generator_test extends advanced_testcase {
/**
* Test rubric creation.
*/
public function test_rubric_creation(): void {
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
// Data for testing.
$name = 'myfirstrubric';
$description = 'My first rubric';
$criteria = [
'Alphabet' => [
'Not known' => 0,
'Letters known but out of order' => 1,
'Letters known in order ascending' => 2,
'Letters known and can recite forwards and backwards' => 4,
],
'Times tables' => [
'Not known' => 0,
'2 times table known' => 2,
'2 and 5 times table known' => 4,
'2, 5, and 10 times table known' => 8,
],
];
// Unit under test.
$this->setUser($user);
$controller = $rubricgenerator->create_instance($context, 'mod_assign', 'submission', $name, $description, $criteria);
$this->assertInstanceOf(gradingform_rubric_controller::class, $controller);
$definition = $controller->get_definition();
$this->assertNotEmpty($definition->id);
$this->assertEquals($name, $definition->name);
$this->assertEquals($description, $definition->description);
$this->assertEquals(gradingform_controller::DEFINITION_STATUS_READY, $definition->status);
$this->assertNotEmpty($definition->timecreated);
$this->assertNotEmpty($definition->timemodified);
$this->assertEquals($user->id, $definition->usercreated);
$this->assertNotEmpty($definition->rubric_criteria);
$this->assertCount(2, $definition->rubric_criteria);
// Check the alphabet criteria.
$criteriaids = array_keys($definition->rubric_criteria);
$alphabet = $definition->rubric_criteria[$criteriaids[0]];
$this->assertNotEmpty($alphabet['id']);
$this->assertEquals(1, $alphabet['sortorder']);
$this->assertEquals('Alphabet', $alphabet['description']);
$this->assertNotEmpty($alphabet['levels']);
$levels = $alphabet['levels'];
$levelids = array_keys($levels);
$level = $levels[$levelids[0]];
$this->assertEquals(0, $level['score']);
$this->assertEquals('Not known', $level['definition']);
$level = $levels[$levelids[1]];
$this->assertEquals(1, $level['score']);
$this->assertEquals('Letters known but out of order', $level['definition']);
$level = $levels[$levelids[2]];
$this->assertEquals(2, $level['score']);
$this->assertEquals('Letters known in order ascending', $level['definition']);
$level = $levels[$levelids[3]];
$this->assertEquals(4, $level['score']);
$this->assertEquals('Letters known and can recite forwards and backwards', $level['definition']);
// Check the times tables criteria.
$tables = $definition->rubric_criteria[$criteriaids[1]];
$this->assertNotEmpty($tables['id']);
$this->assertEquals(2, $tables['sortorder']);
$this->assertEquals('Times tables', $tables['description']);
$this->assertNotEmpty($tables['levels']);
$levels = $tables['levels'];
$levelids = array_keys($levels);
$level = $levels[$levelids[0]];
$this->assertEquals(0, $level['score']);
$this->assertEquals('Not known', $level['definition']);
$level = $levels[$levelids[1]];
$this->assertEquals(2, $level['score']);
$this->assertEquals('2 times table known', $level['definition']);
$level = $levels[$levelids[2]];
$this->assertEquals(4, $level['score']);
$this->assertEquals('2 and 5 times table known', $level['definition']);
$level = $levels[$levelids[3]];
$this->assertEquals(8, $level['score']);
$this->assertEquals('2, 5, and 10 times table known', $level['definition']);
}
/**
* Test the get_level_and_criterion_for_values function.
* This is used for finding criterion and level information within a rubric.
*/
public function test_get_level_and_criterion_for_values(): void {
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
// Data for testing.
$description = 'My first rubric';
$criteria = [
'Alphabet' => [
'Not known' => 0,
'Letters known but out of order' => 1,
'Letters known in order ascending' => 2,
'Letters known and can recite forwards and backwards' => 4,
],
'Times tables' => [
'Not known' => 0,
'2 times table known' => 2,
'2 and 5 times table known' => 4,
'2, 5, and 10 times table known' => 8,
],
];
$this->setUser($user);
$controller = $rubricgenerator->create_instance($context, 'mod_assign', 'submission', 'rubric', $description, $criteria);
// Valid criterion and level.
$result = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Alphabet', 2);
$this->assertEquals('Alphabet', $result['criterion']->description);
$this->assertEquals('2', $result['level']->score);
$this->assertEquals('Letters known in order ascending', $result['level']->definition);
// Valid criterion. Invalid level.
$result = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Alphabet', 3);
$this->assertEquals('Alphabet', $result['criterion']->description);
$this->assertNull($result['level']);
// Invalid criterion.
$result = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Foo', 0);
$this->assertNull($result['criterion']);
}
/**
* Tests for the get_test_rubric function.
*/
public function test_get_test_rubric(): void {
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
$this->setUser($user);
$rubric = $rubricgenerator->get_test_rubric($context, 'assign', 'submissions');
$definition = $rubric->get_definition();
$this->assertEquals('testrubric', $definition->name);
$this->assertEquals('Description text', $definition->description);
$this->assertEquals(gradingform_controller::DEFINITION_STATUS_READY, $definition->status);
// Should create a rubric with 2 criterion.
$this->assertCount(2, $definition->rubric_criteria);
}
/**
* Test the get_submitted_form_data function.
*/
public function test_get_submitted_form_data(): void {
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
$this->setUser($user);
$controller = $rubricgenerator->get_test_rubric($context, 'assign', 'submissions');
$result = $rubricgenerator->get_submitted_form_data($controller, 93, [
'Spelling is important' => [
'score' => 1,
'remark' => 'Good speeling',
],
'Pictures' => [
'score' => 2,
'remark' => 'Lots of nice pictures!',
]
]);
$this->assertIsArray($result);
$this->assertEquals(93, $result['itemid']);
$this->assertIsArray($result['criteria']);
$this->assertCount(2, $result['criteria']);
$spelling = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Spelling is important', 1);
$this->assertIsArray($result['criteria'][$spelling['criterion']->id]);
$this->assertEquals($spelling['level']->id, $result['criteria'][$spelling['criterion']->id]['levelid']);
$this->assertEquals('Good speeling', $result['criteria'][$spelling['criterion']->id]['remark']);
$pictures = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Pictures', 2);
$this->assertIsArray($result['criteria'][$pictures['criterion']->id]);
$this->assertEquals($pictures['level']->id, $result['criteria'][$pictures['criterion']->id]['levelid']);
$this->assertEquals('Lots of nice pictures!', $result['criteria'][$pictures['criterion']->id]['remark']);
}
/**
* Test the get_test_form_data function.
*/
public function test_get_test_form_data(): void {
$this->resetAfterTest(true);
// Fetch generators.
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
// Create items required for testing.
$course = $generator->create_course();
$module = $generator->create_module('assign', ['course' => $course]);
$user = $generator->create_user();
$context = context_module::instance($module->cmid);
$this->setUser($user);
$controller = $rubricgenerator->get_test_rubric($context, 'assign', 'submissions');
// Unit under test.
$result = $rubricgenerator->get_test_form_data(
$controller,
1839,
1, 'Propper good speling',
0, 'ASCII art is not a picture'
);
$this->assertIsArray($result);
$this->assertEquals(1839, $result['itemid']);
$this->assertIsArray($result['criteria']);
$this->assertCount(2, $result['criteria']);
$spelling = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Spelling is important', 1);
$this->assertIsArray($result['criteria'][$spelling['criterion']->id]);
$this->assertEquals($spelling['level']->id, $result['criteria'][$spelling['criterion']->id]['levelid']);
$this->assertEquals('Propper good speling', $result['criteria'][$spelling['criterion']->id]['remark']);
$pictures = $rubricgenerator->get_level_and_criterion_for_values($controller, 'Pictures', 0);
$this->assertIsArray($result['criteria'][$pictures['criterion']->id]);
$this->assertEquals($pictures['level']->id, $result['criteria'][$pictures['criterion']->id]['levelid']);
$this->assertEquals('ASCII art is not a picture', $result['criteria'][$pictures['criterion']->id]['remark']);
}
}
@@ -0,0 +1,418 @@
<?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/>.
declare(strict_types = 1);
namespace gradingform_rubric\grades\grader\gradingpanel\external;
use advanced_testcase;
use coding_exception;
use core_grades\component_gradeitem;
use core_grades\component_gradeitems;
use core_external\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 {
/**
* 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('warnings', $result);
$this->assertIsArray($result['warnings']);
$this->assertEmpty($result['warnings']);
// Test the grade array items.
$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('usergrade', $result['grade']);
$this->assertEquals('- / 100.00', $result['grade']['usergrade']);
$this->assertArrayHasKey('maxgrade', $result['grade']);
$this->assertIsInt($result['grade']['maxgrade']);
$this->assertEquals(100, $result['grade']['maxgrade']);
$this->assertArrayHasKey('gradedby', $result['grade']);
$this->assertEquals(null, $result['grade']['gradedby']);
$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();
[
'forum' => $forum,
'controller' => $controller,
'definition' => $definition,
'student' => $student,
'teacher' => $teacher,
] = $this->get_test_data();
$this->execute_and_assert_fetch($forum, $controller, $definition, $teacher, $teacher, $student);
}
/**
* Class mates should not get other's grades.
*/
public function test_execute_fetch_does_not_return_data_to_other_students(): void {
$this->resetAfterTest();
[
'forum' => $forum,
'controller' => $controller,
'definition' => $definition,
'student' => $student,
'teacher' => $teacher,
'course' => $course,
] = $this->get_test_data();
$evilstudent = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->expectException(\required_capability_exception::class);
$this->execute_and_assert_fetch($forum, $controller, $definition, $evilstudent, $teacher, $student);
}
/**
* Grades can be returned to graded user.
*/
public function test_execute_fetch_return_data_to_graded_user(): void {
$this->resetAfterTest();
[
'forum' => $forum,
'controller' => $controller,
'definition' => $definition,
'student' => $student,
'teacher' => $teacher,
] = $this->get_test_data();
$this->execute_and_assert_fetch($forum, $controller, $definition, $student, $teacher, $student);
}
/**
* Executes and performs all the assertions of the fetch method with the given parameters.
*/
private function execute_and_assert_fetch($forum, $controller, $definition, $fetcheruser, $grader, $gradeduser) {
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
$this->setUser($grader);
$gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
$grade = $gradeitem->get_grade_for_user($gradeduser, $grader);
$instance = $gradeitem->get_advanced_grading_instance($grader, $grade);
$submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $gradeduser->id,
0, 'Too many mistakes. Please try again.',
2, 'Great number of pictures. Well done.'
);
$gradeitem->store_grade_from_formdata($gradeduser, $grader, (object) [
'instanceid' => $instance->get_id(),
'advancedgrading' => $submissiondata,
]);
$this->setUser($fetcheruser);
$result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $gradeduser->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('warnings', $result);
$this->assertIsArray($result['warnings']);
$this->assertEmpty($result['warnings']);
// Test the grade array items.
$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('usergrade', $result['grade']);
$this->assertEquals('50.00 / 100.00', $result['grade']['usergrade']);
$this->assertArrayHasKey('maxgrade', $result['grade']);
$this->assertIsInt($result['grade']['maxgrade']);
$this->assertEquals(100, $result['grade']['maxgrade']);
$this->assertArrayHasKey('gradedby', $result['grade']);
$this->assertEquals(fullname($grader), $result['grade']['gradedby']);
$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, 'grade_forum' => 100]));
$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();
// In the situation of mod_forum this would be the id from forum_grades.
$itemid = 1;
$instance = $controller->create_instance($student->id, $itemid);
$data = $this->get_test_form_data(
$controller,
$itemid,
1, 'This user made several mistakes.',
0, 'Please add more pictures.'
);
// Update this instance with data.
$instance->update($data);
return [
'forum' => $forum,
'controller' => $controller,
'definition' => $definition,
'student' => $student,
'teacher' => $teacher,
'course' => $course,
];
}
/**
* Fetch a set of sample data.
*
* @param \gradingform_rubric_controller $controller
* @param int $itemid
* @param float $spellingscore
* @param string $spellingremark
* @param float $picturescore
* @param string $pictureremark
* @return array
*/
protected function get_test_form_data(
\gradingform_rubric_controller $controller,
int $itemid,
float $spellingscore,
string $spellingremark,
float $picturescore,
string $pictureremark
): array {
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
return $rubricgenerator->get_test_form_data(
$controller,
$itemid,
$spellingscore,
$spellingremark,
$picturescore,
$pictureremark
);
}
}
@@ -0,0 +1,257 @@
<?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/>.
declare(strict_types = 1);
namespace gradingform_rubric\grades\grader\gradingpanel\external;
use advanced_testcase;
use coding_exception;
use core_grades\component_gradeitem;
use core_external\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 {
/**
* 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, false, '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, false, '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, false, '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, false, '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, false, $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('warnings', $result);
$this->assertIsArray($result['warnings']);
$this->assertEmpty($result['warnings']);
// Test the grade array items.
$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('usergrade', $result['grade']);
$this->assertEquals('1.00 / 2.00', $result['grade']['usergrade']);
$this->assertArrayHasKey('maxgrade', $result['grade']);
$this->assertIsInt($result['grade']['maxgrade']);
$this->assertEquals(2, $result['grade']['maxgrade']);
$this->assertArrayHasKey('gradedby', $result['grade']);
$this->assertEquals(fullname($teacher), $result['grade']['gradedby']);
$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,
];
}
}
@@ -0,0 +1,182 @@
<?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/>.
/**
* Privacy tests for gradingform_rubric
*
* @package gradingform_rubric
* @category test
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace gradingform_rubric\privacy;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\writer;
use gradingform_rubric\privacy\provider;
use gradingform_rubric_controller;
use context_module;
/**
* Privacy tests for gradingform_rubric
*
* @copyright 2018 Adrian Greeve <adriangreeve.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
/**
* Test the export of rubric data.
*/
public function test_get_gradingform_export_data(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$modulecontext = context_module::instance($module->cmid);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Generate a test rubric and get its controller.
$controller = $this->get_test_rubric($modulecontext, 'assign', 'submissions');
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
$data = $this->get_test_form_data(
$controller,
$itemid,
1, 'This user made several mistakes.',
0, 'Please add more pictures.'
);
// Update this instance with data.
$instance->update($data);
$instanceid = $instance->get_data('id');
// Let's try the method we are testing.
provider::export_gradingform_instance_data($modulecontext, $instance->get_id(), ['Test']);
$data = (array) writer::with_context($modulecontext)->get_data(['Test', 'Rubric', $instanceid]);
$this->assertCount(2, $data);
$this->assertEquals('Spelling is important', $data['Spelling is important']->description);
$this->assertEquals('This user made several mistakes.', $data['Spelling is important']->remark);
$this->assertEquals('Pictures', $data['Pictures']->description);
$this->assertEquals('Please add more pictures.', $data['Pictures']->remark);
}
/**
* Test the deletion of rubric user information via the instance ID.
*/
public function test_delete_gradingform_for_instances(): void {
global $DB;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
$modulecontext = context_module::instance($module->cmid);
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Generate a test rubric and get its controller.
$controller = $this->get_test_rubric($modulecontext, 'assign', 'submissions');
// In the situation of mod_assign this would be the id from assign_grades.
$itemid = 1;
$instance = $controller->create_instance($user->id, $itemid);
$data = $this->get_test_form_data(
$controller,
$itemid,
1, 'This user made several mistakes.',
0, 'Please add more pictures.'
);
// Update this instance with data.
$instance->update($data);
// Second instance.
$itemid = 2;
$instance = $controller->create_instance($user->id, $itemid);
$data = $this->get_test_form_data(
$controller,
$itemid,
0, 'Too many mistakes. Please try again.',
2, 'Great number of pictures. Well done.'
);
// Update this instance with data.
$instance->update($data);
// Check how many records we have in the fillings table.
$records = $DB->get_records('gradingform_rubric_fillings');
$this->assertCount(4, $records);
// Let's delete one of the instances (the last one would be the easiest).
provider::delete_gradingform_for_instances([$instance->get_id()]);
$records = $DB->get_records('gradingform_rubric_fillings');
$this->assertCount(2, $records);
foreach ($records as $record) {
$this->assertNotEquals($instance->get_id(), $record->instanceid);
}
}
/**
* Generate a rubric controller with sample data required for testing of this class.
*
* @param context_module $context
* @param string $component
* @param string $area
* @return gradingform_rubric_controller
*/
protected function get_test_rubric(context_module $context, string $component, string $area): gradingform_rubric_controller {
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
return $rubricgenerator->get_test_rubric($context, $component, $area);
}
/**
* Fetch a set of sample data.
*
* @param gradingform_rubric_controller $controller
* @param int $itemid
* @param float $spellingscore
* @param string $spellingremark
* @param float $picturescore
* @param string $pictureremark
* @return array
*/
protected function get_test_form_data(
gradingform_rubric_controller $controller,
int $itemid,
float $spellingscore,
string $spellingremark,
float $picturescore,
string $pictureremark
): array {
$generator = \testing_util::get_data_generator();
$rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
return $rubricgenerator->get_test_form_data(
$controller,
$itemid,
$spellingscore,
$spellingremark,
$picturescore,
$pictureremark
);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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/>.
/**
* Version information for plugin gradingform_rubric
*
* @package gradingform_rubric
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'gradingform_rubric';
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->maturity = MATURITY_STABLE;
+22
View File
@@ -0,0 +1,22 @@
This files describes API changes in /grade/grading/form/* - Advanced grading methods
information provided here is intended especially for developers.
=== 3.10 ===
* Removed gradingform_provider.
* Removed the following deprecated functions:
get_gradingform_export_data
delete_gradingform_for_context
delete_gradingform_for_userid
=== 3.6 ===
* The privacy interface gradingform_provider has been deprecated. Please use
gradingform_provider_v2 instead.
=== 2.5.2 ===
* Grading methods now can return grade with decimals. See API functions
gradingform_controller::set_grade_range() and
gradingform_controller::get_allow_grade_decimals(), and also examples
in gradingform_rubric_instance::get_grade().
+681
View File
@@ -0,0 +1,681 @@
<?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/>.
/**
* Advanced grading methods support
*
* @package core_grading
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
use core_grades\component_gradeitems;
/**
* Factory method returning an instance of the grading manager
*
* There are basically ways how to use this factory method. If the area record
* id is known to the caller, get the manager for that area by providing just
* the id. If the area record id is not know, the context, component and area name
* can be provided. Note that null values are allowed in the second case as the context,
* component and the area name can be set explicitly later.
*
* @category grading
* @example $manager = get_grading_manager($areaid);
* @example $manager = get_grading_manager(context_system::instance());
* @example $manager = get_grading_manager($context, 'mod_assign', 'submission');
* @param stdClass|int|null $context_or_areaid if $areaid is passed, no other parameter is needed
* @param string|null $component the frankenstyle name of the component
* @param string|null $area the name of the gradable area
* @return grading_manager
*/
function get_grading_manager($context_or_areaid = null, $component = null, $area = null) {
global $DB;
$manager = new grading_manager();
if (is_object($context_or_areaid)) {
$context = $context_or_areaid;
} else {
$context = null;
if (is_numeric($context_or_areaid)) {
$manager->load($context_or_areaid);
return $manager;
}
}
if (!is_null($context)) {
$manager->set_context($context);
}
if (!is_null($component)) {
$manager->set_component($component);
}
if (!is_null($area)) {
$manager->set_area($area);
}
return $manager;
}
/**
* General class providing access to common grading features
*
* Grading manager provides access to the particular grading method controller
* in that area.
*
* Fully initialized instance of the grading manager operates over a single
* gradable area. It is possible to work with a partially initialized manager
* that knows just context and component without known area, for example.
* It is also possible to change context, component and area of an existing
* manager. Such pattern is used when copying form definitions, for example.
*
* @package core_grading
* @copyright 2011 David Mudrak <david@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @category grading
*/
class grading_manager {
/** @var stdClass the context */
protected $context;
/** @var string the frankenstyle name of the component */
protected $component;
/** @var string the name of the gradable area */
protected $area;
/** @var stdClass|false|null the raw record from {grading_areas}, false if does not exist, null if invalidated cache */
private $areacache = null;
/**
* Returns grading manager context
*
* @return stdClass grading manager context
*/
public function get_context() {
return $this->context;
}
/**
* Sets the context the manager operates on
*
* @param stdClass $context
*/
public function set_context(stdClass $context) {
$this->areacache = null;
$this->context = $context;
}
/**
* Returns grading manager component
*
* @return string grading manager component
*/
public function get_component() {
return $this->component;
}
/**
* Sets the component the manager operates on
*
* @param string $component the frankenstyle name of the component
*/
public function set_component($component) {
$this->areacache = null;
list($type, $name) = core_component::normalize_component($component);
$this->component = $type.'_'.$name;
}
/**
* Returns grading manager area name
*
* @return string grading manager area name
*/
public function get_area() {
return $this->area;
}
/**
* Sets the area the manager operates on
*
* @param string $area the name of the gradable area
*/
public function set_area($area) {
$this->areacache = null;
$this->area = $area;
}
/**
* Returns a text describing the context and the component
*
* At the moment this works for gradable areas in course modules. In the future, this
* method should be improved so it works for other contexts (blocks, gradebook items etc)
* or subplugins.
*
* @return string
*/
public function get_component_title() {
$this->ensure_isset(array('context', 'component'));
if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
if ($this->get_component() == 'core_grading') {
$title = ''; // we are in the bank UI
} else {
throw new coding_exception('Unsupported component at the system context');
}
} else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
list($context, $course, $cm) = get_context_info_array($this->get_context()->id);
if ($cm && strval($cm->name) !== '') {
$title = format_string($cm->name, true, array('context' => $context));
} else {
debugging('Gradable areas are currently supported at the course module level only', DEBUG_DEVELOPER);
$title = $this->get_component();
}
} else {
throw new coding_exception('Unsupported gradable area context level');
}
return $title;
}
/**
* Returns the localized title of the currently set area
*
* @return string
*/
public function get_area_title() {
if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
return '';
} else if ($this->get_context()->contextlevel >= CONTEXT_COURSE) {
$this->ensure_isset(array('context', 'component', 'area'));
$areas = $this->get_available_areas();
if (array_key_exists($this->get_area(), $areas)) {
return $areas[$this->get_area()];
} else {
debugging('Unknown area!');
return '???';
}
} else {
throw new coding_exception('Unsupported context level');
}
}
/**
* Loads the gradable area info from the database
*
* @param int $areaid
*/
public function load($areaid) {
global $DB;
$this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
$this->context = context::instance_by_id($this->areacache->contextid, MUST_EXIST);
$this->component = $this->areacache->component;
$this->area = $this->areacache->areaname;
}
/**
* Returns the list of installed grading plugins together, optionally extended
* with a simple direct grading.
*
* @param bool $includenone should the 'Simple direct grading' be included
* @return array of the (string)name => (string)localized title of the method
*/
public static function available_methods($includenone = true) {
if ($includenone) {
$list = array('' => get_string('gradingmethodnone', 'core_grading'));
} else {
$list = array();
}
foreach (core_component::get_plugin_list('gradingform') as $name => $location) {
$list[$name] = get_string('pluginname', 'gradingform_'.$name);
}
return $list;
}
/**
* Returns the list of available grading methods in the given context
*
* Currently this is just a static list obtained from {@link self::available_methods()}.
* In the future, the list of available methods may be controlled per-context.
*
* Requires the context property to be set in advance.
*
* @param bool $includenone should the 'Simple direct grading' be included
* @return array of the (string)name => (string)localized title of the method
*/
public function get_available_methods($includenone = true) {
$this->ensure_isset(array('context'));
return self::available_methods($includenone);
}
/**
* Returns the list of gradable areas provided by the given component
*
* This performs a callback to the library of the relevant plugin to obtain
* the list of supported areas.
*
* @param string $component normalized component name
* @return array of (string)areacode => (string)localized title of the area
*/
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') {
$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');
}
}
/**
* Returns the list of gradable areas in the given context and component
*
* This performs a callback to the library of the relevant plugin to obtain
* the list of supported areas.
* @return array of (string)areacode => (string)localized title of the area
*/
public function get_available_areas() {
global $CFG;
$this->ensure_isset(array('context', 'component'));
if ($this->get_context()->contextlevel == CONTEXT_SYSTEM) {
if ($this->get_component() !== 'core_grading') {
throw new coding_exception('Unsupported component at the system context');
} else {
return array();
}
} else if ($this->get_context()->contextlevel == CONTEXT_MODULE) {
$modulecontext = $this->get_context();
$coursecontext = $modulecontext->get_course_context();
$cm = get_fast_modinfo($coursecontext->instanceid)->get_cm($modulecontext->instanceid);
return self::available_areas("mod_{$cm->modname}");
} else {
throw new coding_exception('Unsupported gradable area context level');
}
}
/**
* Returns the currently active grading method in the gradable area
*
* @return string|null the name of the grading plugin of null if it has not been set
*/
public function get_active_method() {
global $DB;
$this->ensure_isset(array('context', 'component', 'area'));
// get the current grading area record if it exists
if (is_null($this->areacache)) {
$this->areacache = $DB->get_record('grading_areas', array(
'contextid' => $this->context->id,
'component' => $this->component,
'areaname' => $this->area),
'*', IGNORE_MISSING);
}
if ($this->areacache === false) {
// no area record yet
return null;
}
return $this->areacache->activemethod;
}
/**
* Sets the currently active grading method in the gradable area
*
* @param string $method the method name, eg 'rubric' (must be available)
* @return bool true if the method changed or was just set, false otherwise
*/
public function set_active_method($method) {
global $DB;
$this->ensure_isset(array('context', 'component', 'area'));
// make sure the passed method is empty or a valid plugin name
if (empty($method)) {
$method = null;
} else {
if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
throw new moodle_exception('invalid_method_name', 'core_grading');
}
$available = $this->get_available_methods(false);
if (!array_key_exists($method, $available)) {
throw new moodle_exception('invalid_method_name', 'core_grading');
}
}
// get the current grading area record if it exists
if (is_null($this->areacache)) {
$this->areacache = $DB->get_record('grading_areas', array(
'contextid' => $this->context->id,
'component' => $this->component,
'areaname' => $this->area),
'*', IGNORE_MISSING);
}
$methodchanged = false;
if ($this->areacache === false) {
// no area record yet, create one with the active method set
$area = array(
'contextid' => $this->context->id,
'component' => $this->component,
'areaname' => $this->area,
'activemethod' => $method);
$DB->insert_record('grading_areas', $area);
$methodchanged = true;
} else {
// update the existing record if needed
if ($this->areacache->activemethod !== $method) {
$DB->set_field('grading_areas', 'activemethod', $method, array('id' => $this->areacache->id));
$methodchanged = true;
}
}
$this->areacache = null;
return $methodchanged;
}
/**
* Extends the settings navigation with the grading settings
*
* This function is called when the context for the page is an activity module with the
* FEATURE_ADVANCED_GRADING and the user has the permission moodle/grade:managegradingforms.
*
* @param settings_navigation $settingsnav {@link settings_navigation}
* @param navigation_node $modulenode {@link navigation_node}
*/
public function extend_settings_navigation(settings_navigation $settingsnav, navigation_node $modulenode=null) {
$this->ensure_isset(array('context', 'component'));
$areas = $this->get_available_areas();
if (empty($areas)) {
// no money, no funny
return;
} else {
// make just a single node for the management screen
$areatitle = reset($areas);
$areaname = key($areas);
$this->set_area($areaname);
$managementnode = $modulenode->add(get_string('gradingmanagement', 'core_grading'),
$this->get_management_url(), settings_navigation::TYPE_CUSTOM, null, 'advgrading');
}
}
/**
* Extends the module navigation with the advanced grading information
*
* This function is called when the context for the page is an activity module with the
* FEATURE_ADVANCED_GRADING.
*
* @param global_navigation $navigation
* @param navigation_node $modulenode
*/
public function extend_navigation(global_navigation $navigation, navigation_node $modulenode=null) {
$this->ensure_isset(array('context', 'component'));
$areas = $this->get_available_areas();
foreach ($areas as $areaname => $areatitle) {
$this->set_area($areaname);
if ($controller = $this->get_active_controller()) {
$controller->extend_navigation($navigation, $modulenode);
}
}
}
/**
* Returns the given method's controller in the gradable area
*
* @param string $method the method name, eg 'rubric' (must be available)
* @return gradingform_controller
*/
public function get_controller($method) {
global $CFG, $DB;
$this->ensure_isset(array('context', 'component', 'area'));
// make sure the passed method is a valid plugin name
if ('gradingform_'.$method !== clean_param('gradingform_'.$method, PARAM_COMPONENT)) {
throw new moodle_exception('invalid_method_name', 'core_grading');
}
$available = $this->get_available_methods(false);
if (!array_key_exists($method, $available)) {
throw new moodle_exception('invalid_method_name', 'core_grading');
}
// get the current grading area record if it exists
if (is_null($this->areacache)) {
$this->areacache = $DB->get_record('grading_areas', array(
'contextid' => $this->context->id,
'component' => $this->component,
'areaname' => $this->area),
'*', IGNORE_MISSING);
}
if ($this->areacache === false) {
// no area record yet, create one
$area = array(
'contextid' => $this->context->id,
'component' => $this->component,
'areaname' => $this->area);
$areaid = $DB->insert_record('grading_areas', $area);
// reload the cache
$this->areacache = $DB->get_record('grading_areas', array('id' => $areaid), '*', MUST_EXIST);
}
require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php');
$classname = 'gradingform_'.$method.'_controller';
return new $classname($this->context, $this->component, $this->area, $this->areacache->id);
}
/**
* Returns the controller for the active method if it is available
*
* @return null|gradingform_controller
*/
public function get_active_controller() {
if ($gradingmethod = $this->get_active_method()) {
$controller = $this->get_controller($gradingmethod);
if ($controller->is_form_available()) {
return $controller;
}
}
return null;
}
/**
* Returns the URL of the grading area management page
*
* @param moodle_url $returnurl optional URL of the page where the user should be sent back to
* @return moodle_url
*/
public function get_management_url(moodle_url $returnurl = null) {
$this->ensure_isset(array('context', 'component'));
if ($this->areacache) {
$params = array('areaid' => $this->areacache->id);
} else {
$params = array('contextid' => $this->context->id, 'component' => $this->component);
if ($this->area) {
$params['area'] = $this->area;
}
}
if (!is_null($returnurl)) {
$params['returnurl'] = $returnurl->out(false);
}
return new moodle_url('/grade/grading/manage.php', $params);
}
/**
* Creates a new shared area to hold a grading form template
*
* Shared area are implemented as virtual gradable areas at the system level context
* with the component set to core_grading and unique random area name.
*
* @param string $method the name of the plugin we create the area for
* @return int the new area id
*/
public function create_shared_area($method) {
global $DB;
// generate some unique random name for the new area
$name = $method . '_' . sha1(rand().uniqid($method, true));
// create new area record
$area = array(
'contextid' => context_system::instance()->id,
'component' => 'core_grading',
'areaname' => $name,
'activemethod' => $method);
return $DB->insert_record('grading_areas', $area);
}
/**
* Removes all data associated with the given context
*
* This is called by {@link context::delete_content()}
*
* @param int $contextid context id
*/
public static function delete_all_for_context($contextid) {
global $DB;
$areaids = $DB->get_fieldset_select('grading_areas', 'id', 'contextid = ?', array($contextid));
$methods = array_keys(self::available_methods(false));
foreach($areaids as $areaid) {
$manager = get_grading_manager($areaid);
foreach ($methods as $method) {
$controller = $manager->get_controller($method);
$controller->delete_definition();
}
}
$DB->delete_records_list('grading_areas', 'id', $areaids);
}
/**
* Helper method to tokenize the given string
*
* Splits the given string into smaller strings. This is a helper method for
* full text searching in grading forms. If the given string is surrounded with
* double quotes, the resulting array consists of a single item containing the
* quoted content.
*
* Otherwise, string like 'grammar, english language' would be tokenized into
* the three tokens 'grammar', 'english', 'language'.
*
* One-letter tokens like are dropped in non-phrase mode. Repeated tokens are
* returned just once.
*
* @param string $needle
* @return array
*/
public static function tokenize($needle) {
// check if we are searching for the exact phrase
if (preg_match('/^[\s]*"[\s]*(.*?)[\s]*"[\s]*$/', $needle, $matches)) {
$token = $matches[1];
if ($token === '') {
return array();
} else {
return array($token);
}
}
// split the needle into smaller parts separated by non-word characters
$tokens = preg_split("/\W/u", $needle);
// keep just non-empty parts
$tokens = array_filter($tokens);
// distinct
$tokens = array_unique($tokens);
// drop one-letter tokens
foreach ($tokens as $ix => $token) {
if (strlen($token) == 1) {
unset($tokens[$ix]);
}
}
return array_values($tokens);
}
// //////////////////////////////////////////////////////////////////////////
/**
* Make sure that the given properties were set to some not-null value
*
* @param array $properties the list of properties
* @throws coding_exception
*/
private function ensure_isset(array $properties) {
foreach ($properties as $property) {
if (!isset($this->$property)) {
throw new coding_exception('The property "'.$property.'" is not set.');
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More