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,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;