Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

MDL-31731 - new grading form - Marking Guide

  • Loading branch information...
commit 77143217f2d01b3d2d306e0ddb2dd79094e64104 1 parent aa753ac
@danmarsden danmarsden authored
View
4 grade/grading/form/guide/README
@@ -0,0 +1,4 @@
+Marking Guide grading form written by Dan Marsden <dan@danmarsden.com>
+
+based on Lightwork Rubric type 2 format and the spec available here:
+http://docs.moodle.org/dev/Lightwork
View
122 grade/grading/form/guide/backup/moodle2/backup_gradingform_guide_plugin.class.php
@@ -0,0 +1,122 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Support for backup API
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Defines marking guide backup structures
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class backup_gradingform_guide_plugin extends backup_gradingform_plugin {
+
+ /**
+ * Declares marking guide structures to append to the grading form definition
+ * @return backup_plugin_element
+ */
+ protected function define_definition_plugin_structure() {
+
+ // Append data only if the grand-parent element has 'method' set to 'guide'.
+ $plugin = $this->get_plugin_element(null, '../../method', 'guide');
+
+ // Create a visible container for our data.
+ $pluginwrapper = new backup_nested_element($this->get_recommended_name());
+
+ // Connect our visible container to the parent.
+ $plugin->add_child($pluginwrapper);
+
+ // Define our elements.
+
+ $criteria = new backup_nested_element('guidecriteria');
+
+ $criterion = new backup_nested_element('guidecriterion', array('id'), array(
+ 'sortorder', 'shortname', 'description', 'descriptionformat',
+ 'descriptionmarkers', 'descriptionmarkersformat', 'maxscore'));
+
+ $comments = new backup_nested_element('guidecomments');
+
+ $comment = new backup_nested_element('guidecomment', array('id'), array(
+ 'sortorder', 'description', 'descriptionformat'));
+
+ // Build elements hierarchy.
+
+ $pluginwrapper->add_child($criteria);
+ $criteria->add_child($criterion);
+ $criteria->add_child($comments);
+ $comments->add_child($comment);
+
+ // Set sources to populate the data.
+
+ $criterion->set_source_table('gradingform_guide_criteria',
+ array('definitionid' => backup::VAR_PARENTID));
+
+ $comment->set_source_table('gradingform_guide_comments',
+ array('definitionid' => backup::VAR_PARENTID));
+
+ // No need to annotate ids or files yet (one day when criterion definition supports
+ // embedded files, they must be annotated here).
+
+ return $plugin;
+ }
+
+ /**
+ * Declares marking guide structures to append to the grading form instances
+ * @return backup_plugin_element
+ */
+ protected function define_instance_plugin_structure() {
+
+ // Append data only if the ancestor 'definition' element has 'method' set to 'guide'.
+ $plugin = $this->get_plugin_element(null, '../../../../method', 'guide');
+
+ // Create a visible container for our data.
+ $pluginwrapper = new backup_nested_element($this->get_recommended_name());
+
+ // Connect our visible container to the parent.
+ $plugin->add_child($pluginwrapper);
+
+ // Define our elements.
+
+ $fillings = new backup_nested_element('fillings');
+
+ $filling = new backup_nested_element('filling', array('id'), array(
+ 'criterionid', 'remark', 'remarkformat', 'score'));
+
+ // Build elements hierarchy.
+
+ $pluginwrapper->add_child($fillings);
+ $fillings->add_child($filling);
+
+ // Set sources to populate the data.
+
+ $filling->set_source_table('gradingform_guide_fillings',
+ array('instanceid' => backup::VAR_PARENTID));
+
+ // No need to annotate ids or files yet (one day when remark field supports
+ // embedded fileds, they must be annotated here).
+
+ return $plugin;
+ }
+}
View
116 grade/grading/form/guide/backup/moodle2/restore_gradingform_guide_plugin.class.php
@@ -0,0 +1,116 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Support for restore API
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Restores the marking guide specific data from grading.xml file
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class restore_gradingform_guide_plugin extends restore_gradingform_plugin {
+
+ /**
+ * Declares the marking guide XML paths attached to the form definition element
+ *
+ * @return array of {@link restore_path_element}
+ */
+ protected function define_definition_plugin_structure() {
+
+ $paths = array();
+
+ $paths[] = new restore_path_element('gradingform_guide_criterion',
+ $this->get_pathfor('/guidecriteria/guidecriterion'));
+
+ $paths[] = new restore_path_element('gradingform_guide_comment',
+ $this->get_pathfor('/guidecomments/guidecomment'));
+
+ return $paths;
+ }
+
+ /**
+ * Declares the marking guide XML paths attached to the form instance element
+ *
+ * @return array of {@link restore_path_element}
+ */
+ protected function define_instance_plugin_structure() {
+
+ $paths = array();
+
+ $paths[] = new restore_path_element('gradinform_guide_filling',
+ $this->get_pathfor('/fillings/filling'));
+
+ return $paths;
+ }
+
+ /**
+ * Processes criterion element data
+ *
+ * Sets the mapping 'gradingform_guide_criterion' to be used later by
+ * {@link self::process_gradinform_guide_filling()}
+ *
+ * @param array|stdClass $data
+ */
+ public function process_gradingform_guide_criterion($data) {
+ global $DB;
+
+ $data = (object)$data;
+ $oldid = $data->id;
+ $data->definitionid = $this->get_new_parentid('grading_definition');
+
+ $newid = $DB->insert_record('gradingform_guide_criteria', $data);
+ $this->set_mapping('gradingform_guide_criterion', $oldid, $newid);
+ }
+
+ /**
+ * Processes comments element data
+ *
+ * @param array|stdClass $data The data to insert as a comment
+ */
+ public function process_gradingform_guide_comment($data) {
+ global $DB;
+
+ $data = (object)$data;
+ $data->definitionid = $this->get_new_parentid('grading_definition');
+
+ $DB->insert_record('gradingform_guide_comments', $data);
+ }
+
+ /**
+ * Processes filling element data
+ *
+ * @param array|stdClass $data The data to insert as a filling
+ */
+ public function process_gradinform_guide_filling($data) {
+ global $DB;
+
+ $data = (object)$data;
+ $data->instanceid = $this->get_new_parentid('grading_instance');
+ $data->criterionid = $this->get_mappingid('gradingform_guide_criterion', $data->criterionid);
+
+ $DB->insert_record('gradingform_guide_fillings', $data);
+ }
+}
View
54 grade/grading/form/guide/db/install.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<XMLDB PATH="grade/grading/form/markingguide/db" VERSION="20120404" COMMENT="XMLDB file for Moodle marking guide"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
+ >
+ <TABLES>
+ <TABLE NAME="gradingform_guide_criteria" COMMENT="Stores the rows of the criteria grid." NEXT="gradingform_guide_fillings">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="definitionid"/>
+ <FIELD NAME="definitionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the form definition this criterion is part of" PREVIOUS="id" NEXT="sortorder"/>
+ <FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Defines the order of the criterion in the guide" PREVIOUS="definitionid" NEXT="shortname"/>
+ <FIELD NAME="shortname" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="shortname of this criterion" PREVIOUS="sortorder" NEXT="description"/>
+ <FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The criterion description for students" PREVIOUS="shortname" NEXT="descriptionformat"/>
+ <FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the description field" PREVIOUS="description" NEXT="descriptionmarkers"/>
+ <FIELD NAME="descriptionmarkers" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Description for Markers" PREVIOUS="descriptionformat" NEXT="descriptionmarkersformat"/>
+ <FIELD NAME="descriptionmarkersformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" PREVIOUS="descriptionmarkers" NEXT="maxscore"/>
+ <FIELD NAME="maxscore" TYPE="number" LENGTH="10" NOTNULL="true" SEQUENCE="false" DECIMALS="5" COMMENT="maximum grade that can be assigned using this criterion" PREVIOUS="descriptionmarkersformat"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="fk_definitionid"/>
+ <KEY NAME="fk_definitionid" TYPE="foreign" FIELDS="definitionid" REFTABLE="grading_definitions" REFFIELDS="id" PREVIOUS="primary"/>
+ </KEYS>
+ </TABLE>
+ <TABLE NAME="gradingform_guide_fillings" COMMENT="Stores the data of how the guide is filled by a particular rater" PREVIOUS="gradingform_guide_criteria" NEXT="gradingform_guide_comments">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="instanceid"/>
+ <FIELD NAME="instanceid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the grading form instance" PREVIOUS="id" NEXT="criterionid"/>
+ <FIELD NAME="criterionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the criterion (row) in the guide" PREVIOUS="instanceid" NEXT="remark"/>
+ <FIELD NAME="remark" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Side note feedback regarding this particular criterion" PREVIOUS="criterionid" NEXT="remarkformat"/>
+ <FIELD NAME="remarkformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the remark field" PREVIOUS="remark" NEXT="score"/>
+ <FIELD NAME="score" TYPE="number" LENGTH="10" NOTNULL="true" SEQUENCE="false" DECIMALS="5" COMMENT="The score assigned" PREVIOUS="remarkformat"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="fk_instanceid"/>
+ <KEY NAME="fk_instanceid" TYPE="foreign" FIELDS="instanceid" REFTABLE="grading_instances" REFFIELDS="id" PREVIOUS="primary" NEXT="fk_criterionid"/>
+ <KEY NAME="fk_criterionid" TYPE="foreign" FIELDS="criterionid" REFTABLE="gradingform_guide_criteria" REFFIELDS="id" PREVIOUS="fk_instanceid" NEXT="uq_instance_criterion"/>
+ <KEY NAME="uq_instance_criterion" TYPE="unique" FIELDS="instanceid, criterionid" PREVIOUS="fk_criterionid"/>
+ </KEYS>
+ </TABLE>
+ <TABLE NAME="gradingform_guide_comments" COMMENT="frequently used comments used in marking guide" PREVIOUS="gradingform_guide_fillings">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true" NEXT="definitionid"/>
+ <FIELD NAME="definitionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The ID of the form definition this faq is part of" PREVIOUS="id" NEXT="sortorder"/>
+ <FIELD NAME="sortorder" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Defines the order of the comments" PREVIOUS="definitionid" NEXT="description"/>
+ <FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="The comment description" PREVIOUS="sortorder" NEXT="descriptionformat"/>
+ <FIELD NAME="descriptionformat" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="The format of the description field" PREVIOUS="description"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="fk_definitionid"/>
+ <KEY NAME="fk_definitionid" TYPE="foreign" FIELDS="definitionid" REFTABLE="grading_definitions" REFFIELDS="id" PREVIOUS="primary"/>
+ </KEYS>
+ </TABLE>
+ </TABLES>
+</XMLDB>
View
62 grade/grading/form/guide/edit.php
@@ -0,0 +1,62 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Rubric editor page
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(dirname(dirname(dirname(dirname(dirname(__FILE__))))).'/config.php');
+require_once(dirname(__FILE__).'/lib.php');
+require_once(dirname(__FILE__).'/edit_form.php');
+require_once($CFG->dirroot.'/grade/grading/lib.php');
+
+$areaid = required_param('areaid', PARAM_INT);
+
+$manager = get_grading_manager($areaid);
+
+list($context, $course, $cm) = get_context_info_array($manager->get_context()->id);
+
+require_login($course, true, $cm);
+require_capability('moodle/grade:managegradingforms', $context);
+
+$controller = $manager->get_controller('guide');
+
+$PAGE->set_url(new moodle_url('/grade/grading/form/guide/edit.php', array('areaid' => $areaid)));
+$PAGE->set_title(get_string('definemarkingguide', 'gradingform_guide'));
+$PAGE->set_heading(get_string('definemarkingguide', 'gradingform_guide'));
+
+$mform = new gradingform_guide_editguide(null, array('areaid' => $areaid, 'context' => $context,
+ 'allowdraft' => !$controller->has_active_instances()), 'post', '', array('class' => 'gradingform_guide_editform'));
+$data = $controller->get_definition_for_editing(true);
+
+$returnurl = optional_param('returnurl', $manager->get_management_url(), PARAM_LOCALURL);
+$data->returnurl = $returnurl;
+$mform->set_data($data);
+if ($mform->is_cancelled()) {
+ redirect($returnurl);
+} else if ($mform->is_submitted() && $mform->is_validated() && !$mform->need_confirm_regrading($controller)) {
+ // Everything ok, validated, re-grading confirmed if needed. Make changes to the rubric.
+ $controller->update_definition($mform->get_data());
+ redirect($returnurl);
+}
+
+echo $OUTPUT->header();
+$mform->display();
+echo $OUTPUT->footer();
View
220 grade/grading/form/guide/edit_form.php
@@ -0,0 +1,220 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * The form used at the guide editor page is defined here
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/lib/formslib.php');
+require_once(dirname(__FILE__).'/guideeditor.php');
+MoodleQuickForm::registerElementType('guideeditor', $CFG->dirroot.'/grade/grading/form/guide/guideeditor.php',
+ 'moodlequickform_guideeditor');
+
+/**
+ * Defines the guide edit form
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradingform_guide_editguide extends moodleform {
+
+ /**
+ * Form element definition
+ */
+ public function definition() {
+ $form = $this->_form;
+
+ $form->addElement('hidden', 'areaid');
+ $form->setType('areaid', PARAM_INT);
+
+ $form->addElement('hidden', 'returnurl');
+
+ // Name.
+ $form->addElement('text', 'name', get_string('name', 'gradingform_guide'), array('size'=>52));
+ $form->addRule('name', get_string('required'), 'required');
+ $form->setType('name', PARAM_TEXT);
+
+ // Description.
+ $options = gradingform_guide_controller::description_form_field_options($this->_customdata['context']);
+ $form->addElement('editor', 'description_editor', get_string('descriptionstudents', 'gradingform_guide'), null, $options);
+ $form->setType('description_editor', PARAM_RAW);
+
+ // Guide completion status.
+ $choices = array();
+ $choices[gradingform_controller::DEFINITION_STATUS_DRAFT] = html_writer::tag('span',
+ get_string('statusdraft', 'core_grading'), array('class' => 'status draft'));
+ $choices[gradingform_controller::DEFINITION_STATUS_READY] = html_writer::tag('span',
+ get_string('statusready', 'core_grading'), array('class' => 'status ready'));
+ $form->addElement('select', 'status', get_string('guidestatus', 'gradingform_guide'), $choices)->freeze();
+
+ // Guide editor.
+ $element = $form->addElement('guideeditor', 'guide', get_string('pluginname', 'gradingform_guide'));
+ $form->setType('guide', PARAM_RAW);
+
+ $buttonarray = array();
+ $buttonarray[] = &$form->createElement('submit', 'saveguide', get_string('saveguide', 'gradingform_guide'));
+ if ($this->_customdata['allowdraft']) {
+ $buttonarray[] = &$form->createElement('submit', 'saveguidedraft', get_string('saveguidedraft', 'gradingform_guide'));
+ }
+ $editbutton = &$form->createElement('submit', 'editguide', ' ');
+ $editbutton->freeze();
+ $buttonarray[] = &$editbutton;
+ $buttonarray[] = &$form->createElement('cancel');
+ $form->addGroup($buttonarray, 'buttonar', '', array(' '), false);
+ $form->closeHeaderBefore('buttonar');
+ }
+
+ /**
+ * Setup the form depending on current values. This method is called after definition(),
+ * data submission and set_data().
+ * All form setup that is dependent on form values should go in here.
+ *
+ * We remove the element status if there is no current status (i.e. guide is only being created)
+ * so the users do not get confused
+ */
+ public function definition_after_data() {
+ $form = $this->_form;
+ $el = $form->getElement('status');
+ if (!$el->getValue()) {
+ $form->removeElement('status');
+ } else {
+ $vals = array_values($el->getValue());
+ if ($vals[0] == gradingform_controller::DEFINITION_STATUS_READY) {
+ $this->findbutton('saveguide')->setValue(get_string('save', 'gradingform_guide'));
+ }
+ }
+ }
+
+ /**
+ * Form vlidation.
+ * If there are errors return array of errors ("fieldname"=>"error message"),
+ * otherwise true if ok.
+ *
+ * @param array $data array of ("fieldname"=>value) of submitted data
+ * @param array $files array of uploaded files "element_name"=>tmp_file_path
+ * @return array of "element_name"=>"error_description" if there are errors,
+ * or an empty array if everything is OK (true allowed for backwards compatibility too).
+ */
+ public function validation($data, $files) {
+ $err = parent::validation($data, $files);
+ $err = array();
+ $form = $this->_form;
+ $guideel = $form->getElement('guide');
+ if ($guideel->non_js_button_pressed($data['guide'])) {
+ // If JS is disabled and button such as 'Add criterion' is pressed - prevent from submit.
+ $err['guidedummy'] = 1;
+ } else if (isset($data['editguide'])) {
+ // Continue editing.
+ $err['guidedummy'] = 1;
+ } else if ((isset($data['saveguide']) && $data['saveguide']) ||
+ (isset($data['saveguidedraft']) && $data['saveguidedraft'])) {
+ // If user attempts to make guide active - it needs to be validated.
+ if ($guideel->validate($data['guide']) !== false) {
+ $err['guidedummy'] = 1;
+ }
+ }
+ return $err;
+ }
+
+ /**
+ * Return submitted data if properly submitted or returns NULL if validation fails or
+ * if there is no submitted data.
+ *
+ * @return object submitted data; NULL if not valid or not submitted or cancelled
+ */
+ public function get_data() {
+ $data = parent::get_data();
+ if (!empty($data->saveguide)) {
+ $data->status = gradingform_controller::DEFINITION_STATUS_READY;
+ } else if (!empty($data->saveguidedraft)) {
+ $data->status = gradingform_controller::DEFINITION_STATUS_DRAFT;
+ }
+ return $data;
+ }
+
+ /**
+ * Check if there are changes in the guide and it is needed to ask user whether to
+ * mark the current grades for re-grading. User may confirm re-grading and continue,
+ * return to editing or cancel the changes
+ *
+ * @param gradingform_guide_controller $controller
+ */
+ public function need_confirm_regrading($controller) {
+ $data = $this->get_data();
+ if (isset($data->guide['regrade'])) {
+ // We have already displayed the confirmation on the previous step.
+ return false;
+ }
+ if (!isset($data->saveguide) || !$data->saveguide) {
+ // We only need confirmation when button 'Save guide' is pressed.
+ return false;
+ }
+ if (!$controller->has_active_instances()) {
+ // Nothing to re-grade, confirmation not needed.
+ return false;
+ }
+ $changelevel = $controller->update_or_check_guide($data);
+ if ($changelevel == 0) {
+ // No changes in the guide, no confirmation needed.
+ return false;
+ }
+
+ // Freeze form elements and pass the values in hidden fields.
+ // TODO description_editor does not freeze the normal way!
+ $form = $this->_form;
+ foreach (array('guide', 'name'/*, 'description_editor'*/) as $fieldname) {
+ $el =& $form->getElement($fieldname);
+ $el->freeze();
+ $el->setPersistantFreeze(true);
+ if ($fieldname == 'guide') {
+ $el->add_regrade_confirmation($changelevel);
+ }
+ }
+
+ // Replace button text 'saveguide' and unfreeze 'Back to edit' button.
+ $this->findbutton('saveguide')->setValue(get_string('continue'));
+ $el =& $this->findbutton('editguide');
+ $el->setValue(get_string('backtoediting', 'gradingform_guide'));
+ $el->unfreeze();
+
+ return true;
+ }
+
+ /**
+ * Returns a form element (submit button) with the name $elementname
+ *
+ * @param string $elementname
+ * @return HTML_QuickForm_element
+ */
+ protected function &findbutton($elementname) {
+ $form = $this->_form;
+ $buttonar =& $form->getElement('buttonar');
+ $elements =& $buttonar->getElements();
+ foreach ($elements as $el) {
+ if ($el->getName() == $elementname) {
+ return $el;
+ }
+ }
+ return null;
+ }
+}
View
356 grade/grading/form/guide/guideeditor.php
@@ -0,0 +1,356 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * This file contains the marking guide editor element
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once("HTML/QuickForm/input.php");
+
+/**
+ * The editor for the marking guide advanced grading plugin.
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class moodlequickform_guideeditor extends HTML_QuickForm_input {
+ /** @var string help message */
+ public $_helpbutton = '';
+ /** @var null|false|string stores the result of the last validation: null - undefined, false - no errors,
+ * string - error(s) text */
+ protected $validationerrors = null;
+ /** @var bool if element has already been validated **/
+ protected $wasvalidated = false;
+ /** @var null|bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */
+ protected $nonjsbuttonpressed = false;
+ /** @var string|false Message to display in front of the editor (that there exist grades on this guide being edited) */
+ protected $regradeconfirmation = false;
+
+ /**
+ * Constructor
+ *
+ * @param string $elementname
+ * @param string $elementlabel
+ * @param array $attributes
+ */
+ public function moodlequickform_guideeditor($elementname=null, $elementlabel=null, $attributes=null) {
+ parent::HTML_QuickForm_input($elementname, $elementlabel, $attributes);
+ }
+
+ /**
+ * get html for help button
+ *
+ * @return string html for help button
+ */
+ public function getHelpButton() {
+ return $this->_helpbutton;
+ }
+
+ /**
+ * The renderer will take care itself about different display in normal and frozen states
+ *
+ * @return string
+ */
+ public function getElementTemplateType() {
+ return 'default';
+ }
+
+ /**
+ * Specifies that confirmation about re-grading needs to be added to this rubric editor.
+ * $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml()
+ *
+ * @see gradingform_rubric_controller::update_or_check_rubric()
+ * @param int $changelevel
+ */
+ public function add_regrade_confirmation($changelevel) {
+ $this->regradeconfirmation = $changelevel;
+ }
+
+ /**
+ * Returns html string to display this element
+ *
+ * @return string
+ */
+ public function toHtml() {
+ global $PAGE;
+ $html = $this->_getTabs();
+ $renderer = $PAGE->get_renderer('gradingform_guide');
+ $data = $this->prepare_data(null, $this->wasvalidated);
+ if (!$this->_flagFrozen) {
+ $mode = gradingform_guide_controller::DISPLAY_EDIT_FULL;
+ $module = array('name'=>'gradingform_guideeditor',
+ 'fullpath'=>'/grade/grading/form/guide/js/guideeditor.js',
+ 'strings' => array(
+ array('confirmdeletecriterion', 'gradingform_guide'),
+ array('clicktoedit', 'gradingform_guide'),
+ array('clicktoeditname', 'gradingform_guide')
+ ));
+ $PAGE->requires->js_init_call('M.gradingform_guideeditor.init', array(
+ array('name' => $this->getName(),
+ 'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
+ 'commenttemplate' => $renderer->comment_template($mode, $this->getName())
+ )),
+ true, $module);
+ } else {
+ // Guide is frozen, no javascript needed.
+ if ($this->_persistantFreeze) {
+ $mode = gradingform_guide_controller::DISPLAY_EDIT_FROZEN;
+ } else {
+ $mode = gradingform_guide_controller::DISPLAY_PREVIEW;
+ }
+ }
+ if ($this->regradeconfirmation) {
+ if (!isset($data['regrade'])) {
+ $data['regrade'] = 1;
+ }
+ $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
+ }
+ if ($this->validationerrors) {
+ $html .= $renderer->notification($this->validationerrors, 'error');
+ }
+ $html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
+ return $html;
+ }
+ /**
+ * Prepares the data passed in $_POST:
+ * - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled)
+ * sets $this->nonjsbuttonpressed to true/false if such button was pressed
+ * - if options not passed (i.e. we create a new guide) fills the options array with the default values
+ * - if options are passed completes the options array with unchecked checkboxes
+ * - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string
+ * and stores it in $this->validationerrors
+ *
+ * @param array $value
+ * @param boolean $withvalidation whether to enable data validation
+ * @return array
+ */
+ protected function prepare_data($value = null, $withvalidation = false) {
+ if (null === $value) {
+ $value = $this->getValue();
+ }
+ if ($this->nonjsbuttonpressed === null) {
+ $this->nonjsbuttonpressed = false;
+ }
+
+ $errors = array();
+ $return = array('criteria' => array(), 'options' => gradingform_guide_controller::get_default_options(),
+ 'comments' => array());
+ if (!isset($value['criteria'])) {
+ $value['criteria'] = array();
+ $errors['err_nocriteria'] = 1;
+ }
+ // If options are present in $value, replace default values with submitted values.
+ if (!empty($value['options'])) {
+ foreach (array_keys($return['options']) as $option) {
+ // Special treatment for checkboxes.
+ if (!empty($value['options'][$option])) {
+ $return['options'][$option] = $value['options'][$option];
+ } else {
+ $return['options'][$option] = null;
+ }
+
+ }
+ }
+
+ if (is_array($value)) {
+ // For other array keys of $value no special treatmeant neeeded, copy them to return value as is.
+ foreach (array_keys($value) as $key) {
+ if ($key != 'options' && $key != 'criteria' && $key != 'comments') {
+ $return[$key] = $value[$key];
+ }
+ }
+ }
+
+ // Iterate through criteria.
+ $lastaction = null;
+ $lastid = null;
+ foreach ($value['criteria'] as $id => $criterion) {
+ if ($id == 'addcriterion') {
+ $id = $this->get_next_id(array_keys($value['criteria']));
+ $criterion = array('description' => '');
+ $this->nonjsbuttonpressed = true;
+ }
+
+ if ($withvalidation && !array_key_exists('delete', $criterion)) {
+ if (!strlen(trim($criterion['shortname']))) {
+ $errors['err_noshortname'] = 1;
+ $criterion['error_description'] = true;
+ }
+ if (!strlen(trim($criterion['maxscore']))) {
+ $errors['err_nomaxscore'] = 1;
+ $criterion['error_description'] = true;
+ } else if (!is_numeric($criterion['maxscore']) || $criterion['maxscore'] < 0) {
+ $errors['err_maxscorenotnumeric'] = 1;
+ $criterion['error_description'] = true;
+ }
+ }
+ if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
+ unset($criterion['moveup']);
+ if ($lastid !== null) {
+ $lastcriterion = $return['criteria'][$lastid];
+ unset($return['criteria'][$lastid]);
+ $return['criteria'][$id] = $criterion;
+ $return['criteria'][$lastid] = $lastcriterion;
+ } else {
+ $return['criteria'][$id] = $criterion;
+ }
+ $lastaction = null;
+ $lastid = $id;
+ $this->nonjsbuttonpressed = true;
+ } else if (array_key_exists('delete', $criterion)) {
+ $this->nonjsbuttonpressed = true;
+ } else {
+ if (array_key_exists('movedown', $criterion)) {
+ unset($criterion['movedown']);
+ $lastaction = 'movedown';
+ $this->nonjsbuttonpressed = true;
+ }
+ $return['criteria'][$id] = $criterion;
+ $lastid = $id;
+ }
+ }
+
+ // Add sort order field to criteria.
+ $csortorder = 1;
+ foreach (array_keys($return['criteria']) as $id) {
+ $return['criteria'][$id]['sortorder'] = $csortorder++;
+ }
+
+ // Iterate through comments.
+ $lastaction = null;
+ $lastid = null;
+ if (!empty($value['comments'])) {
+ foreach ($value['comments'] as $id => $comment) {
+ if ($id == 'addcomment') {
+ $id = $this->get_next_id(array_keys($value['comments']));
+ $comment = array('description' => '');
+ $this->nonjsbuttonpressed = true;
+ }
+
+ if (array_key_exists('moveup', $comment) || $lastaction == 'movedown') {
+ unset($comment['moveup']);
+ if ($lastid !== null) {
+ $lastcomment = $return['comments'][$lastid];
+ unset($return['comments'][$lastid]);
+ $return['comments'][$id] = $comment;
+ $return['comments'][$lastid] = $lastcomment;
+ } else {
+ $return['comments'][$id] = $comment;
+ }
+ $lastaction = null;
+ $lastid = $id;
+ $this->nonjsbuttonpressed = true;
+ } else if (array_key_exists('delete', $comment)) {
+ $this->nonjsbuttonpressed = true;
+ } else {
+ if (array_key_exists('movedown', $comment)) {
+ unset($comment['movedown']);
+ $lastaction = 'movedown';
+ $this->nonjsbuttonpressed = true;
+ }
+ $return['comments'][$id] = $comment;
+ $lastid = $id;
+ }
+ }
+ // Add sort order field to comments.
+ $csortorder = 1;
+ foreach (array_keys($return['comments']) as $id) {
+ $return['comments'][$id]['sortorder'] = $csortorder++;
+ }
+ }
+ // Create validation error string (if needed).
+ if ($withvalidation) {
+ if (count($errors)) {
+ $rv = array();
+ foreach ($errors as $error => $v) {
+ $rv[] = get_string($error, 'gradingform_guide');
+ }
+ $this->validationerrors = join('<br/ >', $rv);
+ } else {
+ $this->validationerrors = false;
+ }
+ $this->wasvalidated = true;
+ }
+ return $return;
+
+ }
+
+ /**
+ * Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns
+ *
+ * @param array $ids
+ * @return string
+ */
+ protected function get_next_id($ids) {
+ $maxid = 0;
+ foreach ($ids as $id) {
+ if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) {
+ $maxid = (int)$matches[1];
+ }
+ }
+ return 'NEWID'.($maxid+1);
+ }
+
+ /**
+ * Checks if a submit button was pressed which is supposed to be processed on client side by JS
+ * but user seem to have disabled JS in the browser.
+ * (buttons 'add criteria', 'add level', 'move up', 'move down', 'add comment')
+ * In this case the form containing this element is prevented from being submitted
+ *
+ * @param array $value
+ * @return boolean true if non-submit button was pressed and not processed by JS
+ */
+ public function non_js_button_pressed($value) {
+ if ($this->nonjsbuttonpressed === null) {
+ $this->prepare_data($value);
+ }
+ return $this->nonjsbuttonpressed;
+ }
+
+ /**
+ * Validates that guide has at least one criterion, filled definitions and all criteria
+ * have filled descriptions
+ *
+ * @param array $value
+ * @return string|false error text or false if no errors found
+ */
+ public function validate($value) {
+ if (!$this->wasvalidated) {
+ $this->prepare_data($value, true);
+ }
+ return $this->validationerrors;
+ }
+
+ /**
+ * Prepares the data for saving
+ * @see prepare_data()
+ *
+ * @param array $submitvalues
+ * @param boolean $assoc
+ * @return array
+ */
+ public function exportValue(&$submitvalues, $assoc = false) {
+ $value = $this->prepare_data($this->_findValue($submitvalues));
+ return $this->_prepareValue($value, $assoc);
+ }
+}
View
32 grade/grading/form/guide/js/guide.js
@@ -0,0 +1,32 @@
+M.gradingform_guide = {};
+
+/**
+ * This function is called for each guide on page.
+ */
+M.gradingform_guide.init = function(Y, options) {
+ var currentfocus = null;
+
+ Y.all('.markingguideremark').on('blur', function(e) {
+ currentfocus = e.currentTarget;
+ });
+ Y.all('.markingguidecomment').on('click', function(e) {
+ currentfocus.set('value', currentfocus.get('value') + '\n' + e.currentTarget.get('innerHTML'));
+ currentfocus.focus();
+ });
+
+ Y.all('.showmarkerdesc input[type=radio]').on('click', function(e) {
+ if (e.currentTarget.get('value')=='false') {
+ Y.all('.criteriondescriptionmarkers').addClass('hide');
+ } else {
+ Y.all('.criteriondescriptionmarkers').removeClass('hide');
+ }
+ });
+
+ Y.all('.showstudentdesc input[type=radio]').on('click', function(e) {
+ if (e.currentTarget.get('value')=='false') {
+ Y.all('.criteriondescription').addClass('hide');
+ } else {
+ Y.all('.criteriondescription').removeClass('hide');
+ }
+ });
+};
View
252 grade/grading/form/guide/js/guideeditor.js
@@ -0,0 +1,252 @@
+M.gradingform_guideeditor = {'templates' : {}, 'eventhandler' : null, 'name' : null, 'Y' : null};
+
+/**
+ * This function is called for each guideeditor on page.
+ */
+M.gradingform_guideeditor.init = function(Y, options) {
+ M.gradingform_guideeditor.name = options.name
+ M.gradingform_guideeditor.Y = Y
+ M.gradingform_guideeditor.templates[options.name] = {
+ 'criterion' : options.criteriontemplate,
+ 'comment' : options.commenttemplate
+ }
+ M.gradingform_guideeditor.disablealleditors()
+ Y.on('click', M.gradingform_guideeditor.clickanywhere, 'body', null)
+ YUI().use('event-touch', function (Y) {
+ Y.one('body').on('touchstart', M.gradingform_guideeditor.clickanywhere);
+ Y.one('body').on('touchend', M.gradingform_guideeditor.clickanywhere);
+ })
+ M.gradingform_guideeditor.addhandlers()
+};
+
+// Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html
+M.gradingform_guideeditor.addhandlers = function() {
+ var Y = M.gradingform_guideeditor.Y
+ var name = M.gradingform_guideeditor.name
+ if (M.gradingform_guideeditor.eventhandler) {
+ M.gradingform_guideeditor.eventhandler.detach()
+ }
+ M.gradingform_guideeditor.eventhandler = Y.on('click', M.gradingform_guideeditor.buttonclick, '#guide-'+name+' input[type=submit]', null);
+}
+
+// switches all input text elements to non-edit mode
+M.gradingform_guideeditor.disablealleditors = function() {
+ var Y = M.gradingform_guideeditor.Y
+ var name = M.gradingform_guideeditor.name
+ Y.all('#guide-'+name+' .criteria .description input[type=text]:not(.pseudotablink)').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} );
+ Y.all('#guide-'+name+' .criteria .description textarea').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} );
+ Y.all('#guide-'+name+' .comments .description textarea').each( function(node) {M.gradingform_guideeditor.editmode(node, false)} );
+}
+
+// function invoked on each click on the page. If criterion values are clicked
+// it switches the element to edit mode. If guide button is clicked it does nothing so the 'buttonclick'
+// function is invoked
+M.gradingform_guideeditor.clickanywhere = function(e) {
+ if (e.type == 'touchstart') {
+ return
+ }
+ var el = e.target
+ // if clicked on button - disablecurrenteditor, continue
+ if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') {
+ return
+ }
+ // if clicked on description item and this item is not enabled - enable it
+ var container = null
+ if ((container = el.ancestor('.criterionname')) || (container = el.ancestor('.criterionmaxscore'))) {
+ el = container.one('input[type=text]')
+ } else if ((container = el.ancestor('.criteriondesc')) || (container = el.ancestor('.criteriondescmarkers'))) {
+ el = container.one('textarea')
+ } else {
+ el = null
+ }
+ if (el) {
+ if (el.hasClass('hiddenelement')) {
+ M.gradingform_guideeditor.disablealleditors()
+ M.gradingform_guideeditor.editmode(el, true)
+ }
+ return
+ }
+ // else disablecurrenteditor
+ M.gradingform_guideeditor.disablealleditors()
+}
+
+// switch the criterion item to edit mode or switch back
+M.gradingform_guideeditor.editmode = function(el, editmode) {
+ var Y = M.gradingform_guideeditor.Y
+ var ta = el
+ if (!editmode && ta.hasClass('hiddenelement')) {
+ return;
+ }
+ if (editmode && !ta.hasClass('hiddenelement')) {
+ return;
+ }
+ var pseudotablink = '<input type="text" size="1" class="pseudotablink"/>',
+ taplain = ta.next('.plainvalue'),
+ tbplain = null,
+ tb = el.one('.score input[type=text]')
+ // add 'plainvalue' next to textarea for description/definition and next to input text field for score (if applicable)
+ if (!taplain && ta.get('name') != '') {
+ ta.insert('<div class="plainvalue">'+pseudotablink+'<span class="textvalue">&nbsp;</span></div>', 'after')
+ taplain = ta.next('.plainvalue')
+ taplain.one('.pseudotablink').on('focus', M.gradingform_guideeditor.clickanywhere)
+ if (tb) {
+ tb.get('parentNode').append('<span class="plainvalue">'+pseudotablink+'<span class="textvalue">&nbsp;</span></span>')
+ tbplain = tb.get('parentNode').one('.plainvalue')
+ tbplain.one('.pseudotablink').on('focus', M.gradingform_guideeditor.clickanywhere)
+ }
+ }
+ if (tb && !tbplain) {
+ tbplain = tb.get('parentNode').one('.plainvalue')
+ }
+ if (!editmode) {
+ // if we need to hide the input fields, copy their contents to plainvalue(s). If description/definition
+ // is empty, display the default text ('Click to edit ...') and add/remove 'empty' CSS class to element
+ var value = Y.Lang.trim(ta.get('value'));
+ if (value.length) {
+ taplain.removeClass('empty')
+ } else if (ta.get('name').indexOf('[shortname]') > 1){
+ value = M.str.gradingform_guide.clicktoeditname
+ taplain.addClass('editname')
+ } else {
+ value = M.str.gradingform_guide.clicktoedit
+ taplain.addClass('empty')
+ }
+ taplain.one('.textvalue').set('innerHTML', value)
+ if (tb) {
+ tbplain.one('.textvalue').set('innerHTML', tb.get('value'))
+ }
+ // hide/display textarea, textbox and plaintexts
+ taplain.removeClass('hiddenelement')
+ ta.addClass('hiddenelement')
+ if (tb) {
+ tbplain.removeClass('hiddenelement')
+ tb.addClass('hiddenelement')
+ }
+ } else {
+ // if we need to show the input fields, set the width/height for textarea so it fills the cell
+ try {
+ if (ta.get('name').indexOf('[maxscore]') > 1) {
+ ta.setStyle('width', '25px');
+ } else {
+ var width = parseFloat(ta.get('parentNode').getComputedStyle('width'))-10,
+ height = parseFloat(ta.get('parentNode').getComputedStyle('height'))
+ ta.setStyle('width', Math.max(width,50)+'px')
+ ta.setStyle('height', Math.max(height,30)+'px')
+ }
+ }
+ catch (err) {
+ // this browser do not support 'computedStyle', leave the default size of the textbox
+ }
+ // hide/display textarea, textbox and plaintexts
+ taplain.addClass('hiddenelement')
+ ta.removeClass('hiddenelement')
+ if (tb) {
+ tbplain.addClass('hiddenelement')
+ tb.removeClass('hiddenelement')
+ }
+ }
+ // focus the proper input field in edit mode
+ if (editmode) {
+ ta.focus()
+ }
+}
+
+// handler for clicking on submit buttons within guideeditor element. Adds/deletes/rearranges criteria/comments on client side
+M.gradingform_guideeditor.buttonclick = function(e, confirmed) {
+ var Y = M.gradingform_guideeditor.Y
+ var name = M.gradingform_guideeditor.name
+ if (e.target.get('type') != 'submit') {
+ return;
+ }
+ M.gradingform_guideeditor.disablealleditors()
+ var chunks = e.target.get('id').split('-')
+ var section = chunks[1]
+ var action = chunks[chunks.length-1]
+
+ if (chunks[0] != name || (section != 'criteria' && section != 'comments')) {
+ return;
+ }
+ // prepare the id of the next inserted criterion
+
+ if (section == 'criteria') {
+ elements_str = '#guide-'+name+' .criteria .criterion'
+ } else if (section == 'comments') {
+ elements_str = '#guide-'+name+' .comments .criterion'
+ }
+ if (action == 'addcriterion' || action == 'addcomment') {
+ var newid = M.gradingform_guideeditor.calculatenewid(elements_str)
+ }
+ var dialog_options = {
+ 'scope' : this,
+ 'callbackargs' : [e, true],
+ 'callback' : M.gradingform_guideeditor.buttonclick
+ };
+ if (chunks.length == 3 && (action == 'addcriterion' || action == 'addcomment')) {
+ // ADD NEW CRITERION OR COMMENT
+ var parentel = Y.one('#'+name+'-'+section)
+ if (parentel.one('>tbody')) {
+ parentel = parentel.one('>tbody')
+ }
+ if (section == 'criteria') {
+ var newcriterion = M.gradingform_guideeditor.templates[name]['criterion']
+ parentel.append(newcriterion.replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''))
+ } else if (section == 'comments') {
+ var newcomment = M.gradingform_guideeditor.templates[name]['comment']
+ parentel.append(newcomment.replace(/\{COMMENT-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, ''))
+ }
+
+ M.gradingform_guideeditor.addhandlers();
+ M.gradingform_guideeditor.disablealleditors()
+ M.gradingform_guideeditor.assignclasses(elements_str)
+ //M.gradingform_guideeditor.editmode(Y.one('#guide-'+name+' #'+name+'-'+section+'-NEWID'+newid+'-shortname'),true)
+ } else if (chunks.length == 4 && action == 'moveup') {
+ // MOVE UP
+ el = Y.one('#'+name+'-'+section+'-'+chunks[2])
+ if (el.previous()) {
+ el.get('parentNode').insertBefore(el, el.previous())
+ }
+ M.gradingform_guideeditor.assignclasses(elements_str)
+ } else if (chunks.length == 4 && action == 'movedown') {
+ // MOVE DOWN
+ el = Y.one('#'+name+'-'+section+'-'+chunks[2])
+ if (el.next()) {
+ el.get('parentNode').insertBefore(el.next(), el)
+ }
+ M.gradingform_guideeditor.assignclasses(elements_str)
+ } else if (chunks.length == 4 && action == 'delete') {
+ // DELETE
+ if (confirmed) {
+ Y.one('#'+name+'-'+section+'-'+chunks[2]).remove()
+ M.gradingform_guideeditor.assignclasses(elements_str)
+ } else {
+ dialog_options['message'] = M.str.gradingform_guide.confirmdeletecriterion
+ M.util.show_confirm_dialog(e, dialog_options);
+ }
+ } else {
+ // unknown action
+ return;
+ }
+ e.preventDefault();
+}
+
+// properly set classes (first/last/odd/even) and/or criterion sortorder for elements Y.all(elements_str)
+M.gradingform_guideeditor.assignclasses = function (elements_str) {
+ var elements = M.gradingform_guideeditor.Y.all(elements_str)
+ for (var i=0; i<elements.size(); i++) {
+ elements.item(i).removeClass('first').removeClass('last').removeClass('even').removeClass('odd').
+ addClass(((i%2)?'odd':'even') + ((i==0)?' first':'') + ((i==elements.size()-1)?' last':''))
+ elements.item(i).all('input[type=hidden]').each(
+ function(node) {if (node.get('name').match(/sortorder/)) { node.set('value', i)}}
+ );
+ }
+}
+
+// returns unique id for the next added element, it should not be equal to any of Y.all(elements_str) ids
+M.gradingform_guideeditor.calculatenewid = function (elements_str) {
+ var newid = 1
+ M.gradingform_guideeditor.Y.all(elements_str).each( function(node) {
+ var idchunks = node.get('id').split('-'), id = idchunks.pop();
+ if (id.match(/^NEWID(\d+)$/)) { newid = Math.max(newid, parseInt(id.substring(5))+1)};
+ } );
+ return newid
+}
View
82 grade/grading/form/guide/lang/en/gradingform_guide.php
@@ -0,0 +1,82 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Strings for the marking guide advanced grading plugin
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+$string['addcomment'] = 'Add frequently used comment';
+$string['addcriterion'] = 'Add criterion';
+$string['alwaysshowdefinition'] = 'Show guide definition to students';
+$string['backtoediting'] = 'Back to editing';
+$string['clicktocopy'] = 'Click to copy this text into the criteria feedback';
+$string['clicktoedit'] = 'Click to edit';
+$string['clicktoeditname'] = 'Click to edit criterion name';
+$string['comments'] = 'Frequently used comments';
+$string['commentsdelete'] = 'Delete comment';
+$string['commentsempty'] = 'Click to edit comment';
+$string['commentsmovedown'] = 'Move down';
+$string['commentsmoveup'] = 'Move up';
+$string['confirmdeletecriterion'] = 'Are you sure you want to delete this item?';
+$string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
+$string['criterion'] = 'Criterion';
+$string['criteriondelete'] = 'Delete criterion';
+$string['criterionempty'] = 'Click to edit criterion';
+$string['criterionmovedown'] = 'Move down';
+$string['criterionmoveup'] = 'Move up';
+$string['definemarkingguide'] = 'Define marking guide';
+$string['description'] = 'Description';
+$string['descriptionmarkers'] = 'Description for Markers';
+$string['descriptionstudents'] = 'Description for Students';
+$string['err_maxscorenotnumeric'] = 'Criterion max score must be numeric';
+$string['err_nocomment'] = 'Comment can not be empty';
+$string['err_nodescription'] = 'Student description can not be empty';
+$string['err_nodescriptionmarkers'] = 'Marker description can not be empty';
+$string['err_nomaxscore'] = 'Criterion max score can not be empty';
+$string['err_noshortname'] = 'Criterion name can not be empty';
+$string['err_scoreinvalid'] = 'The score given to {$a->criterianame} is not valid, the max score is: {$a->maxscore}';
+$string['gradingof'] = '{$a} grading';
+$string['guidemappingexplained'] = 'WARNING: Your marking guide has a maximum grade of <b>{$a->maxscore} points</b> but the maximum grade set in your activity is {$a->modulegrade} The maximum score set in your marking guide will be scaled up to the maximum grade in the module.<br />
+ Intermediate scores will be converted respectively and rounded to the nearest available grade.';
+$string['guidenotcompleted'] = 'Please provide a valid grade for each criterion';
+$string['guideoptions'] = 'Marking guide options';
+$string['guidestatus'] = 'Current marking guide status';
+$string['hidemarkerdesc'] = 'Hide marker criterion descriptions';
+$string['hidestudentdesc'] = 'Hide student criterion descriptions';
+$string['maxscore'] = 'Maximum mark';
+$string['name'] = 'Name';
+$string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
+$string['pluginname'] = 'Marking guide';
+$string['previewmarkingguide'] = 'Preview marking guide';
+$string['regrademessage1'] = 'You are about to save changes to a marking guide that has already been used for grading. Please indicate if existing grades need to be reviewed. If you set this then the marking guide will be hidden from students until their item is regraded.';
+$string['regrademessage5'] = 'You are about to save significant changes to a marking guide that has already been used for grading. The gradebook value will be unchanged, but the marking guide will be hidden from students until their item is regraded.';
+$string['regradeoption0'] = 'Do not mark for regrade';
+$string['regradeoption1'] = 'Mark for regrade';
+$string['restoredfromdraft'] = 'NOTE: The last attempt to grade this person was not saved properly so draft grades have been restored. If you want to cancel these changes use the \'Cancel\' button below.';
+$string['save'] = 'Save';
+$string['saveguide'] = 'Save marking guide and make it ready';
+$string['saveguidedraft'] = 'Save as draft';
+$string['score'] = 'score';
+$string['showdescriptionstudent'] = 'Display description to those being graded';
+$string['showmarkerdesc'] = 'Show marker criterion descriptions';
+$string['showmarkspercriterionstudents'] = 'Show marks per criterion to students';
+$string['showstudentdesc'] = 'Show student criterion descriptions';
View
880 grade/grading/form/guide/lib.php
@@ -0,0 +1,880 @@
+<?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 guide plugin
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+require_once($CFG->dirroot.'/grade/grading/form/lib.php');
+
+/**
+ * This controller encapsulates the guide grading logic
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradingform_guide_controller extends gradingform_controller {
+ // Modes of displaying the guide (used in gradingform_guide_renderer).
+ /** guide display mode: For editing (moderator or teacher creates a guide) */
+ const DISPLAY_EDIT_FULL = 1;
+ /** guide display mode: Preview the guide design with hidden fields */
+ const DISPLAY_EDIT_FROZEN = 2;
+ /** guide display mode: Preview the guide design (for person with manage permission) */
+ const DISPLAY_PREVIEW = 3;
+ /** guide display mode: Preview the guide (for people being graded) */
+ const DISPLAY_PREVIEW_GRADED= 8;
+ /** guide display mode: For evaluation, enabled (teacher grades a student) */
+ const DISPLAY_EVAL = 4;
+ /** guide display mode: For evaluation, with hidden fields */
+ const DISPLAY_EVAL_FROZEN = 5;
+ /** guide display mode: Teacher reviews filled guide */
+ const DISPLAY_REVIEW = 6;
+ /** guide display mode: Dispaly filled guide (i.e. students see their grades) */
+ const DISPLAY_VIEW = 7;
+
+ /** @var stdClass|false the definition structure */
+ protected $moduleinstance = false;
+
+ /**
+ * Extends the module settings navigation with the guide 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 'guide'.
+ *
+ * @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('definemarkingguide', 'gradingform_guide'),
+ $this->get_editor_url(), settings_navigation::TYPE_CUSTOM,
+ null, null, new pix_icon('icon', '', 'gradingform_guide'));
+ }
+
+ /**
+ * 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}
+ * @return void
+ */
+ 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_guide', 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 guide definition into the database
+ *
+ * @see parent::update_definition()
+ * @param stdClass $newdefinition guide definition data as coming from gradingform_guide_editguide::get_data()
+ * @param int $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_guide($newdefinition, $usermodified, true);
+ if (isset($newdefinition->guide['regrade']) && $newdefinition->guide['regrade']) {
+ $this->mark_for_regrade();
+ }
+ }
+
+ /**
+ * Either saves the guide 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 guide is the same, students still may not require re-grading
+ * 3 - removed criteria 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 guide definition data as coming from gradingform_guide_editguide::get_data()
+ * @param int|null $usermodified optional userid of the author of the definition, defaults to the current user
+ * @param bool $doupdate if true actually updates DB, otherwise performs a check
+ * @return int
+ */
+ public function update_or_check_guide(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->guide['options'])) {
+ $newdefinition->guide['options'] = self::get_default_options();
+ }
+ $newdefinition->options = json_encode($newdefinition->guide['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);
+
+ // Update guide data.
+ $haschanges = array();
+ if (empty($newdefinition->guide['criteria'])) {
+ $newcriteria = array();
+ } else {
+ $newcriteria = $newdefinition->guide['criteria']; // New ones to be saved.
+ }
+ $currentcriteria = $currentdefinition->guide_criteria;
+ $criteriafields = array('sortorder', 'description', 'descriptionformat', 'descriptionmarkers',
+ 'descriptionmarkersformat', 'shortname', 'maxscore');
+ foreach ($newcriteria as $id => $criterion) {
+ if (preg_match('/^NEWID\d+$/', $id)) {
+ // Insert criterion into DB.
+ $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE,
+ 'descriptionmarkersformat' => FORMAT_MOODLE); // TODO 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_guide_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_guide_criteria', $data);
+ }
+ $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_guide_criteria', array('id' => $id));
+ }
+ $haschanges[3] = true;
+ }
+ }
+ // Now handle comments.
+ if (empty($newdefinition->guide['comments'])) {
+ $newcomment = array();
+ } else {
+ $newcomment = $newdefinition->guide['comments']; // New ones to be saved.
+ }
+ $currentcomments = $currentdefinition->guide_comment;
+ $commentfields = array('sortorder', 'description');
+ foreach ($newcomment as $id => $comment) {
+ if (preg_match('/^NEWID\d+$/', $id)) {
+ // Insert criterion into DB.
+ $data = array('definitionid' => $this->definition->id, 'descriptionformat' => FORMAT_MOODLE);
+ foreach ($commentfields as $key) {
+ if (array_key_exists($key, $comment)) {
+ $data[$key] = $comment[$key];
+ }
+ }
+ if ($doupdate) {
+ $id = $DB->insert_record('gradingform_guide_comments', $data);
+ }
+ } else {
+ // Update criterion in DB.
+ $data = array();
+ foreach ($commentfields as $key) {
+ if (array_key_exists($key, $comment) && $comment[$key] != $currentcomments[$id][$key]) {
+ $data[$key] = $comment[$key];
+ }
+ }
+ if (!empty($data)) {
+ // Update only if something is changed.
+ $data['id'] = $id;
+ if ($doupdate) {
+ $DB->update_record('gradingform_guide_comments', $data);
+ }
+ }
+ }
+ }
+ // Remove deleted criteria from DB.
+ foreach (array_keys($currentcomments) as $id) {
+ if (!array_key_exists($id, $newcomment)) {
+ if ($doupdate) {
+ $DB->delete_records('gradingform_guide_comments', array('id' => $id));
+ }
+ }
+ }
+ // End comments handle.
+ 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 guide 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 guide form definition if it exists
+ *
+ * There is a new array called 'guide_criteria' appended to the list of parent's definition properties.
+ */
+ protected function load_definition() {
+ global $DB;
+
+ // Check to see if the user prefs have changed - putting here as this function is called on post even when
+ // validation on the page fails. - hard to find a better place to locate this as it is specific to the guide.
+ $showdesc = optional_param('showmarkerdesc', null, PARAM_BOOL); // Check if we need to change pref.
+ $showdescstudent = optional_param('showstudentdesc', null, PARAM_BOOL); // Check if we need to change pref.
+ if ($showdesc !== null) {
+ set_user_preference('gradingform_guide-showmarkerdesc', $showdesc);
+ }
+ if ($showdescstudent !== null) {
+ set_user_preference('gradingform_guide-showstudentdesc', $showdescstudent);
+ }
+
+ // Get definition.
+ $definition = $DB->get_record('grading_definitions', array('areaid' => $this->areaid,
+ 'method' => $this->get_method_name()), '*');
+ if (!$definition) {
+ // The definition doesn't have to exist. It may be that we are only now creating it.
+ $this->definition = false;
+ return false;
+ }
+
+ $this->definition = $definition;
+ // Now get criteria.
+ $this->definition->guide_criteria = array();
+ $this->definition->guide_comment = array();
+ $criteria = $DB->get_recordset('gradingform_guide_criteria', array('definitionid' => $this->definition->id), 'sortorder');
+ foreach ($criteria as $criterion) {
+ foreach (array('id', 'sortorder', 'description', 'descriptionformat',
+ 'maxscore', 'descriptionmarkers', 'descriptionmarkersformat', 'shortname') as $fieldname) {
+ if ($fieldname == 'maxscore') { // Strip any trailing 0.
+ $this->definition->guide_criteria[$criterion->id][$fieldname] = (float)$criterion->{$fieldname};
+ } else {
+ $this->definition->guide_criteria[$criterion->id][$fieldname] = $criterion->{$fieldname};
+ }
+ }
+ }
+ $criteria->close();
+
+ // Now get comments.
+ $comments = $DB->get_recordset('gradingform_guide_comments', array('definitionid' => $this->definition->id), 'sortorder');
+ foreach ($comments as $comment) {
+ foreach (array('id', 'sortorder', 'description', 'descriptionformat') as $fieldname) {
+ $this->definition->guide_comment[$comment->id][$fieldname] = $comment->{$fieldname};
+ }
+ }
+ $comments->close();
+
+ if (empty($this->moduleinstance)) { // Only set if empty.
+ $modulename = $this->get_component();
+ $context = $this->get_context();
+ if (strpos($modulename, 'mod_') === 0) {
+ $dbman = $DB->get_manager();
+ $modulename = substr($modulename, 4);
+ if ($dbman->table_exists($modulename)) {
+ $this->moduleinstance = $DB->get_record($modulename, array('id' => $context->instanceid));
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the default options for the guide display
+ *
+ * @return array
+ */
+ public static function get_default_options() {
+ $options = array(
+ 'alwaysshowdefinition' => 1,
+ 'showmarkspercriterionstudents' => 1,
+ );
+ return $options;
+ }
+
+ /**
+ * Gets the options of this guide definition, fills the missing options with default values
+ *
+ * @return array
+ */
+ public function get_options() {
+ $options = self::get_default_options();
+ if (!empty($this->definition->options)) {
+ $thisoptions = json_decode($this->definition->options);
+ foreach ($thisoptions as $option => $value) {
+ $options[$option] = $value;
+ }
+ }
+ return $options;
+ }
+
+ /**
+ * Converts the current definition into an object suitable for the editor form's set_data()
+ *
+ * @param bool $addemptycriterion whether to add an empty criterion if the guide 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 (isset($this->moduleinstance->grade)) {
+ $properties->modulegrade = $this->moduleinstance->grade;
+ }
+ 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->guide = array('criteria' => array(), 'options' => $this->get_options(), 'comments' => array());
+ if (!empty($definition->guide_criteria)) {
+ $properties->guide['criteria'] = $definition->guide_criteria;
+ } else if (!$definition && $addemptycriterion) {
+ $properties->guide['criteria'] = array('addcriterion' => 1);
+ }
+ if (!empty($definition->guide_comment)) {
+ $properties->guide['comments'] = $definition->guide_comment;
+ } else if (!$definition && $addemptycriterion) {
+ $properties->guide['comments'] = array('addcomment' => 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->guide = array('criteria' => array(), 'options' => $old->guide['options'], 'comments' => array());
+ $newcritid = 1;
+ foreach ($old->guide['criteria'] as $oldcritid => $oldcrit) {
+ unset($oldcrit['id']);
+ $new->guide['criteria']['NEWID'.$newcritid] = $oldcrit;
+ $newcritid++;
+ }
+ $newcomid = 1;
+ foreach ($old->guide['comments'] as $oldcritid => $oldcom) {
+ unset($oldcom['id']);
+ $new->guide['comments']['NEWID'.$newcomid] = $oldcom;
+ $newcomid++;
+ }
+ return $new;
+ }
+
+ /**
+ * Options for displaying the guide description field in the form
+ *
+ * @param context $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_max_upload_file_size($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 guide plugin renderer
+ *
+ * @param moodle_page $page the target page
+ * @return gradingform_guide_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');
+ }
+
+ $output = $this->get_renderer($page);
+ $criteria = $this->definition->guide_criteria;
+ $comments = $this->definition->guide_comment;
+ $options = $this->get_options();
+ $guide = '';
+ if (has_capability('moodle/grade:managegradingforms', $page->context)) {
+ $guide .= $output->display_guide_mapping_explained($this->get_min_max_score());
+ $guide .= $output->display_guide($criteria, $comments, $options, self::DISPLAY_PREVIEW, 'guide');
+ } else {
+ $guide .= $output->display_guide($criteria, $comments, $options, self::DISPLAY_PREVIEW_GRADED, 'guide');
+ }
+
+ return $guide;
+ }
+
+ /**
+ * Deletes the guide 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_guide_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_guide_criteria',
+ array('definitionid' => $this->definition->id), '', 'id'));
+ // Delete critera.
+ $DB->delete_records_list('gradingform_guide_criteria', 'id', $criteria);
+ // Delete comments.
+ $DB->delete_records('gradingform_guide_comments', array('definitionid' => $this->definition->id));
+ }
+
+ /**
+ * 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) {
+ if ($rs = $DB->get_records('grading_instances', array('raterid' => $raterid, 'itemid' => $itemid),
+ 'timemodified DESC', '*', 0, 1)) {
+ $record = reset($rs);
+ $currentinstance = $this->get_current_instance($raterid, $itemid);
+ if ($record->status == gradingform_guide_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 bool $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_guide_criteria} gc ON (gc.definitionid = $gdid)";
+ }
+
+ /**
+ * 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 An array containing two more arrays
+ * Array of search SQL fragments
+ * Array of params for the search fragments
+ */
+ public static function sql_search_where($token) {
+ global $DB;
+
+ $subsql = array();
+ $params = array();
+
+ // Search in guide criteria description.
+ $subsql[] = $DB->sql_like('gc.description', '?', 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 guide
+ *
+ * @return array
+ */
+ public function get_min_max_score() {
+ if (!$this->is_form_available()) {
+ return null;
+ }
+ $returnvalue = array('minscore' => 0, 'maxscore' => 0);
+ $maxscore = 0;
+ foreach ($this->get_definition()->guide_criteria as $id => $criterion) {
+ $maxscore += $criterion['maxscore'];
+ }
+ $returnvalue['maxscore'] = $maxscore;
+ $returnvalue['minscore'] = 0;
+ if (!empty($this->moduleinstance->grade)) {
+ $returnvalue['modulegrade'] = $this->moduleinstance->grade;
+ }
+ return $returnvalue;
+ }
+}
+
+/**
+ * Class to manage one guide grading instance. Stores information and performs actions like
+ * update, copy, validate, submit, etc.
+ *
+ * @package gradingform_guide
+ * @copyright 2012 Dan Marsden <dan@danmarsden.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class gradingform_guide_instance extends gradingform_instance {
+
+ /** @var array */
+ protected $guide;
+
+ /** @var array An array of validation errors */
+ protected $validationerrors = array();
+
+ /**
+ * Deletes this (INCOMPLETE) instance from database.
+ */
+ public function cancel() {
+ global $DB;
+ parent::cancel();
+ $DB->delete_records('gradingform_guide_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_guide_filling();
+ foreach ($currentgrade['criteria'] as $criterionid => $record) {
+ $params = array('instanceid' => $instanceid, 'criterionid' => $criterionid,
+ 'score' => $record['score'], 'remark' => $record['remark'], 'remarkformat' => $record['remarkformat']);
+ $DB->insert_record('gradingform_guide_fillings', $params);
+ }
+ return $instanceid;
+ }
+
+ /**
+ * Validates that guide 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()->guide_criteria;
+ if (!isset($elementvalue['criteria']) || !is_array($elementvalue['criteria']) ||