From 77143217f2d01b3d2d306e0ddb2dd79094e64104 Mon Sep 17 00:00:00 2001 From: Dan Marsden Date: Thu, 19 Apr 2012 16:31:03 +1200 Subject: [PATCH] MDL-31731 - new grading form - Marking Guide --- grade/grading/form/guide/README | 4 + .../backup_gradingform_guide_plugin.class.php | 122 +++ ...restore_gradingform_guide_plugin.class.php | 116 +++ grade/grading/form/guide/db/install.xml | 54 ++ grade/grading/form/guide/edit.php | 62 ++ grade/grading/form/guide/edit_form.php | 220 +++++ grade/grading/form/guide/guideeditor.php | 356 +++++++ grade/grading/form/guide/js/guide.js | 32 + grade/grading/form/guide/js/guideeditor.js | 252 +++++ .../form/guide/lang/en/gradingform_guide.php | 82 ++ grade/grading/form/guide/lib.php | 880 ++++++++++++++++++ grade/grading/form/guide/pix/icon.png | Bin 0 -> 205 bytes grade/grading/form/guide/preview.php | 56 ++ grade/grading/form/guide/renderer.php | 634 +++++++++++++ grade/grading/form/guide/styles.css | 67 ++ grade/grading/form/guide/version.php | 30 + lib/pluginlib.php | 2 +- 17 files changed, 2968 insertions(+), 1 deletion(-) create mode 100644 grade/grading/form/guide/README create mode 100644 grade/grading/form/guide/backup/moodle2/backup_gradingform_guide_plugin.class.php create mode 100644 grade/grading/form/guide/backup/moodle2/restore_gradingform_guide_plugin.class.php create mode 100644 grade/grading/form/guide/db/install.xml create mode 100644 grade/grading/form/guide/edit.php create mode 100644 grade/grading/form/guide/edit_form.php create mode 100644 grade/grading/form/guide/guideeditor.php create mode 100644 grade/grading/form/guide/js/guide.js create mode 100644 grade/grading/form/guide/js/guideeditor.js create mode 100644 grade/grading/form/guide/lang/en/gradingform_guide.php create mode 100644 grade/grading/form/guide/lib.php create mode 100644 grade/grading/form/guide/pix/icon.png create mode 100644 grade/grading/form/guide/preview.php create mode 100644 grade/grading/form/guide/renderer.php create mode 100644 grade/grading/form/guide/styles.css create mode 100644 grade/grading/form/guide/version.php diff --git a/grade/grading/form/guide/README b/grade/grading/form/guide/README new file mode 100644 index 0000000000000..ddbd871cd3581 --- /dev/null +++ b/grade/grading/form/guide/README @@ -0,0 +1,4 @@ +Marking Guide grading form written by Dan Marsden + +based on Lightwork Rubric type 2 format and the spec available here: +http://docs.moodle.org/dev/Lightwork diff --git a/grade/grading/form/guide/backup/moodle2/backup_gradingform_guide_plugin.class.php b/grade/grading/form/guide/backup/moodle2/backup_gradingform_guide_plugin.class.php new file mode 100644 index 0000000000000..75af83aace58d --- /dev/null +++ b/grade/grading/form/guide/backup/moodle2/backup_gradingform_guide_plugin.class.php @@ -0,0 +1,122 @@ +. + +/** + * Support for backup API + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @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 + * @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; + } +} diff --git a/grade/grading/form/guide/backup/moodle2/restore_gradingform_guide_plugin.class.php b/grade/grading/form/guide/backup/moodle2/restore_gradingform_guide_plugin.class.php new file mode 100644 index 0000000000000..dd99814b19d18 --- /dev/null +++ b/grade/grading/form/guide/backup/moodle2/restore_gradingform_guide_plugin.class.php @@ -0,0 +1,116 @@ +. + +/** + * Support for restore API + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @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 + * @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); + } +} diff --git a/grade/grading/form/guide/db/install.xml b/grade/grading/form/guide/db/install.xml new file mode 100644 index 0000000000000..d8aa0b61c10da --- /dev/null +++ b/grade/grading/form/guide/db/install.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+
diff --git a/grade/grading/form/guide/edit.php b/grade/grading/form/guide/edit.php new file mode 100644 index 0000000000000..c4d481009e030 --- /dev/null +++ b/grade/grading/form/guide/edit.php @@ -0,0 +1,62 @@ +. + +/** + * Rubric editor page + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @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(); \ No newline at end of file diff --git a/grade/grading/form/guide/edit_form.php b/grade/grading/form/guide/edit_form.php new file mode 100644 index 0000000000000..a133b4baee59d --- /dev/null +++ b/grade/grading/form/guide/edit_form.php @@ -0,0 +1,220 @@ +. + +/** + * The form used at the guide editor page is defined here + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @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 + * @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; + } +} diff --git a/grade/grading/form/guide/guideeditor.php b/grade/grading/form/guide/guideeditor.php new file mode 100644 index 0000000000000..aebb9822fcab2 --- /dev/null +++ b/grade/grading/form/guide/guideeditor.php @@ -0,0 +1,356 @@ +. + +/** + * This file contains the marking guide editor element + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @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 + * @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('
', $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); + } +} diff --git a/grade/grading/form/guide/js/guide.js b/grade/grading/form/guide/js/guide.js new file mode 100644 index 0000000000000..783ee469b6553 --- /dev/null +++ b/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'); + } + }); +}; \ No newline at end of file diff --git a/grade/grading/form/guide/js/guideeditor.js b/grade/grading/form/guide/js/guideeditor.js new file mode 100644 index 0000000000000..3aeb83be91f23 --- /dev/null +++ b/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 = '', + 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('
'+pseudotablink+' 
', 'after') + taplain = ta.next('.plainvalue') + taplain.one('.pseudotablink').on('focus', M.gradingform_guideeditor.clickanywhere) + if (tb) { + tb.get('parentNode').append(''+pseudotablink+' ') + 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. + +/** + * Strings for the marking guide advanced grading plugin + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @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 {$a->maxscore} points 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.
+ 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'; diff --git a/grade/grading/form/guide/lib.php b/grade/grading/form/guide/lib.php new file mode 100644 index 0000000000000..8cb164adc46a2 --- /dev/null +++ b/grade/grading/form/guide/lib.php @@ -0,0 +1,880 @@ +. + +/** + * Grading method controller for the guide plugin + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @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 + * @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 + * @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']) || + count($elementvalue['criteria']) < count($criteria)) { + return false; + } + // Reset validation errors. + $this->validationerrors = null; + foreach ($criteria as $id => $criterion) { + if (!isset($elementvalue['criteria'][$id]['score']) + || $criterion['maxscore'] < $elementvalue['criteria'][$id]['score'] + || !is_numeric($elementvalue['criteria'][$id]['score'])) { + $this->validationerrors[$id]['score'] = $elementvalue['criteria'][$id]['score']; + } + } + if (!empty($this->validationerrors)) { + return false; + } + return true; + } + + /** + * Retrieves from DB and returns the data how this guide was filled + * + * @param bool $force whether to force DB query even if the data is cached + * @return array + */ + public function get_guide_filling($force = false) { + global $DB; + if ($this->guide === null || $force) { + $records = $DB->get_records('gradingform_guide_fillings', array('instanceid' => $this->get_id())); + $this->guide = array('criteria' => array()); + foreach ($records as $record) { + $record->score = (float)$record->score; // Strip trailing 0. + $this->guide['criteria'][$record->criterionid] = (array)$record; + } + } + return $this->guide; + } + + /** + * 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_guide_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, + 'score' => $record['score'], 'remarkformat' => FORMAT_MOODLE); + if (isset($record['remark'])) { + $newrecord['remark'] = $record['remark']; + } + $DB->insert_record('gradingform_guide_fillings', $newrecord); + } else { + $newrecord = array('id' => $currentgrade['criteria'][$criterionid]['id']); + foreach (array('score', 'remark'/*, 'remarkformat' TODO */) as $key) { + if (isset($record[$key]) && $currentgrade['criteria'][$criterionid][$key] != $record[$key]) { + $newrecord[$key] = $record[$key]; + } + } + if (count($newrecord) > 1) { + $DB->update_record('gradingform_guide_fillings', $newrecord); + } + } + } + foreach ($currentgrade['criteria'] as $criterionid => $record) { + if (!array_key_exists($criterionid, $data['criteria'])) { + $DB->delete_records('gradingform_guide_fillings', array('id' => $record['id'])); + } + } + $this->get_guide_filling(true); + } + + /** + * Calculates the grade to be pushed to the gradebook + * + * @return int the valid grade from $this->get_controller()->get_grade_range() + */ + public function get_grade() { + global $DB, $USER; + $grade = $this->get_guide_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[count($graderange) - 1]; + + $curscore = 0; + foreach ($grade['criteria'] as $record) { + $curscore += $record['score']; + } + return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])* + ($maxgrade-$mingrade), 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) { + if (!$gradingformelement->_flagFrozen) { + $module = array('name'=>'gradingform_guide', 'fullpath'=>'/grade/grading/form/guide/js/guide.js'); + $page->requires->js_init_call('M.gradingform_guide.init', array( + array('name' => $gradingformelement->getName())), true, $module); + $mode = gradingform_guide_controller::DISPLAY_EVAL; + } else { + if ($gradingformelement->_persistantFreeze) { + $mode = gradingform_guide_controller::DISPLAY_EVAL_FROZEN; + } else { + $mode = gradingform_guide_controller::DISPLAY_REVIEW; + } + } + $criteria = $this->get_controller()->get_definition()->guide_criteria; + $comments = $this->get_controller()->get_definition()->guide_comment; + $options = $this->get_controller()->get_options(); + $value = $gradingformelement->getValue(); + $html = ''; + if ($value === null) { + $value = $this->get_guide_filling(); + } else if (!$this->validate_grading_element($value)) { + $html .= html_writer::tag('div', get_string('guidenotcompleted', 'gradingform_guide'), + array('class' => 'gradingform_guide-error')); + if (!empty($this->validationerrors)) { + foreach ($this->validationerrors as $id => $err) { + $a = new stdClass(); + $a->criterianame = $criteria[$id]['shortname']; + $a->maxscore = $criteria[$id]['maxscore']; + $html .= html_writer::tag('div', get_string('err_scoreinvalid', 'gradingform_guide', $a), + array('class' => 'gradingform_guide-error')); + } + } + } + $currentinstance = $this->get_current_instance(); + if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) { + $html .= html_writer::tag('div', get_string('needregrademessage', 'gradingform_guide'), + array('class' => 'gradingform_guide-regrade')); + } + $haschanges = false; + if ($currentinstance) { + $curfilling = $currentinstance->get_guide_filling(); + foreach ($curfilling['criteria'] as $criterionid => $curvalues) { + $value['criteria'][$criterionid]['score'] = $curvalues['score']; + $newremark = null; + $newscore = null; + if (isset($value['criteria'][$criterionid]['remark'])) { + $newremark = $value['criteria'][$criterionid]['remark']; + } + if (isset($value['criteria'][$criterionid]['score'])) { + $newscore = $value['criteria'][$criterionid]['score']; + } + if ($newscore != $curvalues['score'] || $newremark != $curvalues['remark']) { + $haschanges = true; + } + } + } + if ($this->get_data('isrestored') && $haschanges) { + $html .= html_writer::tag('div', get_string('restoredfromdraft', 'gradingform_guide'), + array('class' => 'gradingform_guide-restored')); + } + if (!empty($options['showdescriptionteacher'])) { + $html .= html_writer::tag('div', $this->get_controller()->get_formatted_description(), + array('class' => 'gradingform_guide-description')); + } + $html .= $this->get_controller()->get_renderer($page)->display_guide($criteria, $comments, $options, $mode, + $gradingformelement->getName(), $value, $this->validationerrors); + return $html; + } +} \ No newline at end of file diff --git a/grade/grading/form/guide/pix/icon.png b/grade/grading/form/guide/pix/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f1af82f25125c3d1082c6cc1e65cde81b46d2857 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^NcO z?l9QdFTSY{6yYrJh%9Dc;1&j9Muu5)B!GhKC7!;n?6)~(`K5U_Ov?TY6!P_SaSX9I zot%=8@Z-D#V?ts=LPEu_FPd#FTeg@uUzD|G1A;cYsm?tA|2w})Oi6J`G&T(2kZ|AI saG!g}oZdHE1NARl=H4N6^*}WvgWf48d2wEDRiMEPp00i_>zopr0ITvl_y7O^ literal 0 HcmV?d00001 diff --git a/grade/grading/form/guide/preview.php b/grade/grading/form/guide/preview.php new file mode 100644 index 0000000000000..ba8aa5d56e3a4 --- /dev/null +++ b/grade/grading/form/guide/preview.php @@ -0,0 +1,56 @@ +. + +/** + * Preview marking guide page + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @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); + +$controller = $manager->get_controller('guide'); +$options = $controller->get_options(); + +if (!$controller->is_form_defined() || empty($options['alwaysshowdefinition'])) { + throw new moodle_exception('nopermissions', 'error', '', get_string('previewmarkingguide', 'gradingform_guide')); +} + +$title = get_string('gradingof', 'gradingform_guide', $manager->get_area_title()); +$PAGE->set_url(new moodle_url('/grade/grading/form/guide/preview.php', array('areaid' => $areaid))); +$PAGE->set_title($title); +$PAGE->set_heading($title); + +echo $OUTPUT->header(); +echo $OUTPUT->heading($title); +if (!empty($options['showdescriptionstudent'])) { + echo $OUTPUT->box($controller->get_formatted_description(), 'gradingform_guide-description'); +} +echo $controller->render_preview($PAGE); +echo $OUTPUT->footer(); diff --git a/grade/grading/form/guide/renderer.php b/grade/grading/form/guide/renderer.php new file mode 100644 index 0000000000000..16acab0020631 --- /dev/null +++ b/grade/grading/form/guide/renderer.php @@ -0,0 +1,634 @@ +. + +/** + * Contains the Guide grading form renderer in all of its glory + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Grading method plugin renderer + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class gradingform_guide_renderer extends plugin_renderer_base { + + /** + * This function returns html code for displaying criterion. Depending on $mode it may be the + * code to edit guide, to preview the guide, to evaluate somebody or to review the evaluation. + * + * This function may be called from display_guide() to display the whole guide, or it can be + * called by itself to return a template used by JavaScript to add new empty criteria to the + * guide being designed. + * In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc. + * + * When overriding this function it is very important to remember that all elements of html + * form (in edit or evaluate mode) must have the name $elementname. + * + * Also JavaScript relies on the class names of elements and when developer changes them + * script might stop working. + * + * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller()} + * @param array $options An array of options. + * showmarkspercriterionstudents (bool) If true adds the current score to the display + * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) + * @param array $criterion criterion data + * @param array $value (only in view mode) teacher's feedback on this criterion + * @param array $validationerrors An array containing validation errors to be shown + * @return string + */ + public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $value = null, + $validationerrors = null) { + if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) { + $criterion = array('id' => '{CRITERION-id}', + 'description' => '{CRITERION-description}', + 'sortorder' => '{CRITERION-sortorder}', + 'class' => '{CRITERION-class}', + 'descriptionmarkers' => '{CRITERION-descriptionmarkers}', + 'shortname' => '{CRITERION-shortname}', + 'maxscore' => '{CRITERION-maxscore}'); + } else { + foreach (array('sortorder', 'description', 'class', 'shortname', 'descriptionmarkers', 'maxscore') as $key) { + // Set missing array elements to empty strings to avoid warnings. + if (!array_key_exists($key, $criterion)) { + $criterion[$key] = ''; + } + } + } + + $criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $criterion['class'], + 'id' => '{NAME}-criteria-{CRITERION-id}')); + $descriptionclass = 'description'; + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { + $criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls')); + foreach (array('moveup', 'delete', 'movedown') as $key) { + $value = get_string('criterion'.$key, 'gradingform_guide'); + $button = html_writer::empty_tag('input', array('type' => 'submit', + 'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']', + 'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value, 'tabindex' => -1)); + $criteriontemplate .= html_writer::tag('div', $button, array('class' => $key)); + } + $criteriontemplate .= html_writer::end_tag('td'); // Controls. + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder'])); + + $shortname = html_writer::empty_tag('input', array('type'=> 'text', + 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'value' => htmlspecialchars($criterion['shortname']))); + $shortname = html_writer::tag('div', $shortname, array('class'=>'criterionname')); + $description = html_writer::tag('textarea', htmlspecialchars($criterion['description']), + array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '65', 'rows' => '5')); + $description = html_writer::tag('div', $description, array('class'=>'criteriondesc')); + + $descriptionmarkers = html_writer::tag('textarea', htmlspecialchars($criterion['descriptionmarkers']), + array('name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'cols' => '65', 'rows' => '5')); + $descriptionmarkers = html_writer::tag('div', $descriptionmarkers, array('class'=>'criteriondescmarkers')); + + $maxscore = html_writer::empty_tag('input', array('type'=> 'text', + 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'size' => '3', + 'value' => htmlspecialchars($criterion['maxscore']))); + $maxscore = html_writer::tag('div', $maxscore, array('class'=>'criterionmaxscore')); + } else { + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) { + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder'])); + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'value' => $criterion['shortname'])); + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[criteria][{CRITERION-id}][description]', 'value' => $criterion['description'])); + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'value' => $criterion['descriptionmarkers'])); + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'value' => $criterion['maxscore'])); + } else if ($mode == gradingform_guide_controller::DISPLAY_EVAL || + $mode == gradingform_guide_controller::DISPLAY_VIEW) { + $descriptionclass = 'descriptionreadonly'; + } + $shortname = html_writer::tag('div', $criterion['shortname'], + array('class'=>'criterionshortname', 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]')); + $descmarkerclass = ''; + $descstudentclass = ''; + if ($mode == gradingform_guide_controller::DISPLAY_EVAL) { + if (!get_user_preferences('gradingform_guide-showmarkerdesc', true)) { + $descmarkerclass = ' hide'; + } + if (!get_user_preferences('gradingform_guide-showstudentdesc', true)) { + $descstudentclass = ' hide'; + } + } + $description = html_writer::tag('div', $criterion['description'], + array('class'=>'criteriondescription'.$descstudentclass, + 'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]')); + $descriptionmarkers = html_writer::tag('div', $criterion['descriptionmarkers'], + array('class'=>'criteriondescriptionmarkers'.$descmarkerclass, + 'name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]')); + $maxscore = html_writer::tag('div', $criterion['maxscore'], + array('class'=>'criteriondescriptionscore', 'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]')); + } + + if (isset($criterion['error_description'])) { + $descriptionclass .= ' error'; + } + + $title = html_writer::tag('label', get_string('criterion', 'gradingform_guide'), + array('for'=>'{NAME}[criteria][{CRITERION-id}][shortname]', 'class' => 'criterionnamelabel')); + $title .= $shortname; + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL || + $mode == gradingform_guide_controller::DISPLAY_PREVIEW) { + $title .= html_writer::tag('label', get_string('descriptionstudents', 'gradingform_guide'), + array('for'=>'{NAME}[criteria][{CRITERION-id}][description]')); + $title .= $description; + $title .= html_writer::tag('label', get_string('descriptionmarkers', 'gradingform_guide'), + array('for'=>'{NAME}[criteria][{CRITERION-id}][descriptionmarkers]')); + $title .= $descriptionmarkers; + $title .= html_writer::tag('label', get_string('maxscore', 'gradingform_guide'), + array('for'=>'{NAME}[criteria][{CRITERION-id}][maxscore]')); + $title .= $maxscore; + } else if ($mode == gradingform_guide_controller::DISPLAY_PREVIEW_GRADED || + $mode == gradingform_guide_controller::DISPLAY_VIEW) { + $title .= $description; + } else { + $title .= $description . $descriptionmarkers; + } + $criteriontemplate .= html_writer::tag('td', $title, array('class' => $descriptionclass, + 'id' => '{NAME}-criteria-{CRITERION-id}-shortname')); + + $currentremark = ''; + $currentscore = ''; + if (isset($value['remark'])) { + $currentremark = $value['remark']; + } + if (isset($value['score'])) { + $currentscore = $value['score']; + } + if ($mode == gradingform_guide_controller::DISPLAY_EVAL) { + $scoreclass = ''; + if (!empty($validationerrors[$criterion['id']]['score'])) { + $scoreclass = 'error'; + $currentscore = $validationerrors[$criterion['id']]['score']; // Show invalid score in form. + } + $input = html_writer::tag('textarea', htmlspecialchars($currentremark), + array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '65', 'rows' => '5', + 'class' => 'markingguideremark')); + $criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark')); + $score = html_writer::tag('label', get_string('score', 'gradingform_guide'), + array('for'=>'{NAME}[criteria][{CRITERION-id}][score]', 'class' => $scoreclass)); + $score .= html_writer::empty_tag('input', array('type'=> 'text', + 'name' => '{NAME}[criteria][{CRITERION-id}][score]', 'class' => $scoreclass, + 'size' => '3', 'value' => htmlspecialchars($currentscore))); + $score .= '/'.$maxscore; + + $criteriontemplate .= html_writer::tag('td', $score, array('class' => 'score')); + } else if ($mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) { + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark)); + } else if ($mode == gradingform_guide_controller::DISPLAY_REVIEW || + $mode == gradingform_guide_controller::DISPLAY_VIEW) { + $criteriontemplate .= html_writer::tag('td', $currentremark, array('class' => 'remark')); + if (!empty($options['showmarkspercriterionstudents'])) { + $criteriontemplate .= html_writer::tag('td', htmlspecialchars($currentscore). ' / '.$maxscore, + array('class' => 'score')); + } + } + $criteriontemplate .= html_writer::end_tag('tr'); // Criterion. + + $criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate); + $criteriontemplate = str_replace('{CRITERION-id}', $criterion['id'], $criteriontemplate); + return $criteriontemplate; + } + + /** + * This function returns html code for displaying criterion. Depending on $mode it may be the + * code to edit guide, to preview the guide, to evaluate somebody or to review the evaluation. + * + * This function may be called from display_guide() to display the whole guide, or it can be + * called by itself to return a template used by JavaScript to add new empty criteria to the + * guide being designed. + * In this case it will use macros like {NAME}, {LEVELS}, {CRITERION-id}, etc. + * + * When overriding this function it is very important to remember that all elements of html + * form (in edit or evaluate mode) must have the name $elementname. + * + * Also JavaScript relies on the class names of elements and when developer changes them + * script might stop working. + * + * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller} + * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) + * @param array $comment + * @return string + */ + public function comment_template($mode, $elementname = '{NAME}', $comment = null) { + if ($comment === null || !is_array($comment) || !array_key_exists('id', $comment)) { + $comment = array('id' => '{COMMENT-id}', + 'description' => '{COMMENT-description}', + 'sortorder' => '{COMMENT-sortorder}', + 'class' => '{COMMENT-class}'); + } else { + foreach (array('sortorder', 'description', 'class') as $key) { + // Set missing array elements to empty strings to avoid warnings. + if (!array_key_exists($key, $comment)) { + $criterion[$key] = ''; + } + } + } + $criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $comment['class'], + 'id' => '{NAME}-comments-{COMMENT-id}')); + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { + $criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls')); + foreach (array('moveup', 'delete', 'movedown') as $key) { + $value = get_string('comments'.$key, 'gradingform_guide'); + $button = html_writer::empty_tag('input', array('type' => 'submit', + 'name' => '{NAME}[comments][{COMMENT-id}]['.$key.']', 'id' => '{NAME}-comments-{COMMENT-id}-'.$key, + 'value' => $value, 'title' => $value, 'tabindex' => -1)); + $criteriontemplate .= html_writer::tag('div', $button, array('class' => $key)); + } + $criteriontemplate .= html_writer::end_tag('td'); // Controls. + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder'])); + $description = html_writer::tag('textarea', htmlspecialchars($comment['description']), + array('name' => '{NAME}[comments][{COMMENT-id}][description]', 'cols' => '65', 'rows' => '5')); + $description = html_writer::tag('div', $description, array('class'=>'criteriondesc')); + } else { + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) { + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder'])); + $criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden', + 'name' => '{NAME}[comments][{COMMENT-id}][description]', 'value' => $comment['description'])); + } + if ($mode == gradingform_guide_controller::DISPLAY_EVAL) { + $description = html_writer::tag('span', htmlspecialchars($comment['description']), + array('name' => '{NAME}[comments][{COMMENT-id}][description]', + 'title' => get_string('clicktocopy', 'gradingform_guide'), + 'id' => '{NAME}[comments][{COMMENT-id}]', 'class'=>'markingguidecomment')); + } else { + $description = $comment['description']; + } + } + $descriptionclass = 'description'; + if (isset($comment['error_description'])) { + $descriptionclass .= ' error'; + } + $criteriontemplate .= html_writer::tag('td', $description, array('class' => $descriptionclass, + 'id' => '{NAME}-comments-{COMMENT-id}-description')); + $criteriontemplate .= html_writer::end_tag('tr'); // Criterion. + + $criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate); + $criteriontemplate = str_replace('{COMMENT-id}', $comment['id'], $criteriontemplate); + return $criteriontemplate; + } + /** + * This function returns html code for displaying guide template (content before and after + * criteria list). Depending on $mode it may be the code to edit guide, to preview the guide, + * to evaluate somebody or to review the evaluation. + * + * This function is called from display_guide() to display the whole guide. + * + * When overriding this function it is very important to remember that all elements of html + * form (in edit or evaluate mode) must have the name $elementname. + * + * Also JavaScript relies on the class names of elements and when developer changes them + * script might stop working. + * + * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller} + * @param array $options An array of options provided to {@link gradingform_guide_renderer::guide_edit_options()} + * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) + * @param string $criteriastr evaluated templates for this guide's criteria + * @param string $commentstr + * @return string + */ + protected function guide_template($mode, $options, $elementname, $criteriastr, $commentstr) { + $classsuffix = ''; // CSS suffix for class of the main div. Depends on the mode. + switch ($mode) { + case gradingform_guide_controller::DISPLAY_EDIT_FULL: + $classsuffix = ' editor editable'; + break; + case gradingform_guide_controller::DISPLAY_EDIT_FROZEN: + $classsuffix = ' editor frozen'; + break; + case gradingform_guide_controller::DISPLAY_PREVIEW: + case gradingform_guide_controller::DISPLAY_PREVIEW_GRADED: + $classsuffix = ' editor preview'; + break; + case gradingform_guide_controller::DISPLAY_EVAL: + $classsuffix = ' evaluate editable'; + break; + case gradingform_guide_controller::DISPLAY_EVAL_FROZEN: + $classsuffix = ' evaluate frozen'; + break; + case gradingform_guide_controller::DISPLAY_REVIEW: + $classsuffix = ' review'; + break; + case gradingform_guide_controller::DISPLAY_VIEW: + $classsuffix = ' view'; + break; + } + + $guidetemplate = html_writer::start_tag('div', array('id' => 'guide-{NAME}', + 'class' => 'clearfix gradingform_guide'.$classsuffix)); + $guidetemplate .= html_writer::tag('table', $criteriastr, array('class' => 'criteria', 'id' => '{NAME}-criteria')); + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { + $value = get_string('addcriterion', 'gradingform_guide'); + $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]', + 'id' => '{NAME}-criteria-addcriterion', 'value' => $value, 'title' => $value)); + $guidetemplate .= html_writer::tag('div', $input, array('class' => 'addcriterion')); + } + + if (!empty($commentstr)) { + $guidetemplate .= html_writer::tag('label', get_string('comments', 'gradingform_guide'), + array('for' => '{NAME}-comments', 'class' => 'commentheader')); + $guidetemplate .= html_writer::tag('table', $commentstr, array('class' => 'comments', 'id' => '{NAME}-comments')); + } + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { + $value = get_string('addcomment', 'gradingform_guide'); + $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[comments][addcomment]', + 'id' => '{NAME}-comments-addcomment', 'value' => $value, 'title' => $value)); + $guidetemplate .= html_writer::tag('div', $input, array('class' => 'addcomment')); + } + + $guidetemplate .= $this->guide_edit_options($mode, $options); + $guidetemplate .= html_writer::end_tag('div'); + + return str_replace('{NAME}', $elementname, $guidetemplate); + } + + /** + * Generates html template to view/edit the guide options. Expression {NAME} is used in + * template for the form element name + * + * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller} + * @param array $options + * @return string + */ + protected function guide_edit_options($mode, $options) { + if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL + && $mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN + && $mode != gradingform_guide_controller::DISPLAY_PREVIEW) { + // Options are displayed only for people who can manage. + return; + } + $html = html_writer::start_tag('div', array('class' => 'options')); + $html .= html_writer::tag('div', get_string('guideoptions', 'gradingform_guide'), array('class' => 'optionsheading')); + $attrs = array('type' => 'hidden', 'name' => '{NAME}[options][optionsset]', 'value' => 1); + foreach ($options as $option => $value) { + $html .= html_writer::start_tag('div', array('class' => 'option '.$option)); + $attrs = array('name' => '{NAME}[options]['.$option.']', 'id' => '{NAME}-options-'.$option); + switch ($option) { + case 'sortlevelsasc': + // Display option as dropdown. + $html .= html_writer::tag('span', get_string($option, 'gradingform_guide'), array('class' => 'label')); + $value = (int)(!!$value); // Make sure $value is either 0 or 1. + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) { + $selectoptions = array(0 => get_string($option.'0', 'gradingform_guide'), + 1 => get_string($option.'1', 'gradingform_guide')); + $valuestr = html_writer::select($selectoptions, $attrs['name'], $value, false, array('id' => $attrs['id'])); + $html .= html_writer::tag('span', $valuestr, array('class' => 'value')); + // TODO add here button 'Sort levels'. + } else { + $html .= html_writer::tag('span', get_string($option.$value, 'gradingform_guide'), + array('class' => 'value')); + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) { + $html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value)); + } + } + break; + default: + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN && $value) { + $html .= html_writer::empty_tag('input', $attrs + array('type' => 'hidden', 'value' => $value)); + } + // Display option as checkbox. + $attrs['type'] = 'checkbox'; + $attrs['value'] = 1; + if ($value) { + $attrs['checked'] = 'checked'; + } + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN || + $mode == gradingform_guide_controller::DISPLAY_PREVIEW) { + $attrs['disabled'] = 'disabled'; + unset($attrs['name']); + } + $html .= html_writer::empty_tag('input', $attrs); + $html .= html_writer::tag('label', get_string($option, 'gradingform_guide'), array('for' => $attrs['id'])); + break; + } + $html .= html_writer::end_tag('div'); // Option. + } + $html .= html_writer::end_tag('div'); // Options. + return $html; + } + + /** + * This function returns html code for displaying guide. Depending on $mode it may be the code + * to edit guide, to preview the guide, to evaluate somebody or to review the evaluation. + * + * It is very unlikely that this function needs to be overriden by theme. It does not produce + * any html code, it just prepares data about guide design and evaluation, adds the CSS + * class to elements and calls the functions level_template, criterion_template and + * guide_template + * + * @param array $criteria data about the guide design + * @param array $comments + * @param array $options + * @param int $mode guide display mode, one of gradingform_guide_controller::DISPLAY_* {@link gradingform_guide_controller} + * @param string $elementname the name of the form element (in editor mode) or the prefix for div ids (in view mode) + * @param array $values evaluation result + * @param array $validationerrors + * @return string + */ + public function display_guide($criteria, $comments, $options, $mode, $elementname = null, $values = null, + $validationerrors = null) { + $criteriastr = ''; + $cnt = 0; + foreach ($criteria as $id => $criterion) { + $criterion['class'] = $this->get_css_class_suffix($cnt++, count($criteria) -1); + $criterion['id'] = $id; + if (isset($values['criteria'][$id])) { + $criterionvalue = $values['criteria'][$id]; + } else { + $criterionvalue = null; + } + $criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $criterionvalue, + $validationerrors); + } + $cnt = 0; + $commentstr = ''; + // Check if comments should be displayed. + if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL || + $mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN || + $mode == gradingform_guide_controller::DISPLAY_PREVIEW || + $mode == gradingform_guide_controller::DISPLAY_EVAL || + $mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) { + + foreach ($comments as $id => $comment) { + $comment['id'] = $id; + $comment['class'] = $this->get_css_class_suffix($cnt++, count($comments) -1); + $commentstr .= $this->comment_template($mode, $elementname, $comment); + } + } + $output = $this->guide_template($mode, $options, $elementname, $criteriastr, $commentstr); + if ($mode == gradingform_guide_controller::DISPLAY_EVAL) { + $showdesc = get_user_preferences('gradingform_guide-showmarkerdesc', true); + $showdescstud = get_user_preferences('gradingform_guide-showstudentdesc', true); + $checked1 = array(); + $checked2 = array(); + $checked_s1 = array(); + $checked_s2 = array(); + $checked = array('checked' => 'checked'); + if ($showdesc) { + $checked1 = $checked; + } else { + $checked2 = $checked; + } + if ($showdescstud) { + $checked_s1 = $checked; + } else { + $checked_s2 = $checked; + } + + $radio = html_writer::tag('input', get_string('showmarkerdesc', 'gradingform_guide'), array('type' => 'radio', + 'name' => 'showmarkerdesc', + 'value' => "true")+$checked1); + $radio .= html_writer::tag('input', get_string('hidemarkerdesc', 'gradingform_guide'), array('type' => 'radio', + 'name' => 'showmarkerdesc', + 'value' => "false")+$checked2); + $output .= html_writer::tag('div', $radio, array('class' => 'showmarkerdesc')); + + $radio = html_writer::tag('input', get_string('showstudentdesc', 'gradingform_guide'), array('type' => 'radio', + 'name' => 'showstudentdesc', + 'value' => "true")+$checked_s1); + $radio .= html_writer::tag('input', get_string('hidestudentdesc', 'gradingform_guide'), array('type' => 'radio', + 'name' => 'showstudentdesc', + 'value' => "false")+$checked_s2); + $output .= html_writer::tag('div', $radio, array('class' => 'showstudentdesc')); + } + return $output; + } + + /** + * Help function to return CSS class names for element (first/last/even/odd) with leading space + * + * @param int $idx index of this element in the row/column + * @param int $maxidx maximum index of the element in the row/column + * @return string + */ + protected function get_css_class_suffix($idx, $maxidx) { + $class = ''; + if ($idx == 0) { + $class .= ' first'; + } + if ($idx == $maxidx) { + $class .= ' last'; + } + if ($idx % 2) { + $class .= ' odd'; + } else { + $class .= ' even'; + } + return $class; + } + + /** + * Displays for the student the list of instances or default content if no instances found + * + * @param array $instances array of objects of type gradingform_guide_instance + * @param string $defaultcontent default string that would be displayed without advanced grading + * @param bool $cangrade whether current user has capability to grade in this context + * @return string + */ + public function display_instances($instances, $defaultcontent, $cangrade) { + $return = ''; + if (count($instances)) { + $return .= html_writer::start_tag('div', array('class' => 'advancedgrade')); + $idx = 0; + foreach ($instances as $instance) { + $return .= $this->display_instance($instance, $idx++, $cangrade); + } + $return .= html_writer::end_tag('div'); + } + return $return. $defaultcontent; + } + + /** + * Displays one grading instance + * + * @param gradingform_guide_instance $instance + * @param int $idx unique number of instance on page + * @param bool $cangrade whether current user has capability to grade in this context + */ + public function display_instance(gradingform_guide_instance $instance, $idx, $cangrade) { + $criteria = $instance->get_controller()->get_definition()->guide_criteria; + $options = $instance->get_controller()->get_options(); + $values = $instance->get_guide_filling(); + if ($cangrade) { + $mode = gradingform_guide_controller::DISPLAY_REVIEW; + } else { + $mode = gradingform_guide_controller::DISPLAY_VIEW; + } + + $output = $this->box($instance->get_controller()->get_formatted_description(), 'gradingform_guide-description'). + $this->display_guide($criteria, array(), $options, $mode, 'guide'.$idx, $values); + return $output; + } + + + /** + * Displays a confirmation message after a regrade has occured + * + * @param string $elementname + * @param int $changelevel + * @param int $value The regrade option that was used + * @return string + */ + public function display_regrade_confirmation($elementname, $changelevel, $value) { + $html = html_writer::start_tag('div', array('class' => 'gradingform_guide-regrade')); + if ($changelevel<=2) { + $html .= get_string('regrademessage1', 'gradingform_guide'); + $selectoptions = array( + 0 => get_string('regradeoption0', 'gradingform_guide'), + 1 => get_string('regradeoption1', 'gradingform_guide') + ); + $html .= html_writer::select($selectoptions, $elementname.'[regrade]', $value, false); + } else { + $html .= get_string('regrademessage5', 'gradingform_guide'); + $html .= html_writer::empty_tag('input', array('name' => $elementname.'[regrade]', 'value' => 1, 'type' => 'hidden')); + } + $html .= html_writer::end_tag('div'); + return $html; + } + /** + * Generates and returns HTML code to display information box about how guide score is converted to the grade + * + * @param array $scores + * @return string + */ + public function display_guide_mapping_explained($scores) { + $html = ''; + if (!$scores) { + return $html; + } + if (isset($scores['modulegrade']) && $scores['maxscore'] < $scores['modulegrade']) { + $html .= $this->box(html_writer::tag('div', get_string('guidemappingexplained', 'gradingform_guide', (object)$scores)) + , 'generalbox gradingform_guide-error'); + } + + return $html; + } +} \ No newline at end of file diff --git a/grade/grading/form/guide/styles.css b/grade/grading/form/guide/styles.css new file mode 100644 index 0000000000000..9a18aea84a789 --- /dev/null +++ b/grade/grading/form/guide/styles.css @@ -0,0 +1,67 @@ +.gradingform_guide-regrade {padding:10px;background:#FFDDDD;border:1px solid #F00;margin-bottom:10px;} +.gradingform_guide-restored {padding:10px;background:#FFFFDD;border:1px solid #FF0;margin-bottom:10px;} +.gradingform_guide-error {color:red;font-weight:bold;} + +.gradingform_guide_editform .status {font-weight:normal;text-transform:uppercase;font-size:60%;padding:0.25em;border:1px solid #EEE;} +.gradingform_guide_editform .status.ready {background-color:#e7f1c3;border-color:#AAEEAA;} +.gradingform_guide_editform .status.draft {background-color:#f3f2aa;border-color:#EEEE22;} + +.gradingform_guide.editor .criterion .controls, +.gradingform_guide .criterion .description, +.gradingform_guide .criterion .remark {vertical-align: top;} + +.gradingform_guide.editor .criterion .controls, +.gradingform_guide .criterion .description, +.gradingform_guide .criterion .remark {padding:3px;} + +.gradingform_guide .criteria {height:100%;} +.gradingform_guide .criterion {border:1px solid #DDD;overflow: hidden;} +.gradingform_guide .criterion.even {background:#F0F0F0;} + +.gradingform_guide .criterion .description {width:100%;} +.gradingform_guide .criterion .description .criterionmaxscore input {width:20px;} +.gradingform_guide .criterion .description .criterionname {font-weight:bold;} +.gradingform_guide .criterion label {font-weight:bold; padding-right: 5px} + +.gradingform_guide .plainvalue.empty {font-style: italic; color: #AAA;} +.gradingform_guide .plainvalue.editname {font-weight:bold;} + +/* Make invisible the buttons 'Move up' for the first criterion and 'Move down' for the last, because those buttons will make no change */ +.gradingform_guide.editor .criterion.first.last .controls .delete input, +.gradingform_guide.editor .criterion.first .controls .moveup input, +.gradingform_guide.editor .criterion.last .controls .movedown input {display:none;} + +/* replace buttons with images */ +.gradingform_guide.editor .delete input, +.gradingform_guide.editor .moveup input, +.gradingform_guide.editor .movedown input{text-indent: -1000em;cursor:pointer;border:none;} +.gradingform_guide.editor .criterion .controls .delete input {width:20px;height:16px;background: transparent url([[pix:i/cross_red_big]]) no-repeat center top;} +.gradingform_guide.editor .moveup input {width:20px;height:15px;background: transparent url([[pix:t/up]]) no-repeat center top;margin-top:4px;} +.gradingform_guide.editor .movedown input {width:20px;height:15px;background: transparent url([[pix:t/down]]) no-repeat center top;margin-top:4px;} + +.gradingform_guide.editor .addcriterion input, +.gradingform_guide.editor .addcomment input {background: transparent url([[pix:t/addgreen]]) no-repeat;display:block;color:#555555;font-weight:bold;text-decoration:none;} +.gradingform_guide.editor .addcriterion input, +.gradingform_guide.editor .addcomment input {background-position: 5px 8px;height:30px;line-height:29px;margin-bottom:14px;padding-left:20px;padding-right:10px;} + +.gradingform_guide .options .optionsheading {font-weight:bold;font-size:1.1em;padding-bottom:5px;} +.gradingform_guide .options .option {padding-bottom:2px;} +.gradingform_guide .options .option label {margin-left: 5px;} +.gradingform_guide .options .option .value {margin-left: 5px;font-weight:bold;} + +.gradingform_guide .criterion .description.error {background:#FFDDDD;} + +/* special classes for elements created by guideeditor.js */ +.gradingform_guide.editor .hiddenelement {display:none;} +.gradingform_guide.editor .pseudotablink {background-color:transparent;border:0 solid;height:1px;width:1px;color:transparent;padding:0;margin:0;position:relative;float:right;} + +.gradingform_guide .markingguidecomment {cursor: pointer;} +.jsenabled .gradingform_guide .markingguidecomment:before {content: url([[pix:t/add]]);padding-right:2px;} +.gradingform_guide .commentheader {font-weight:bold;font-size:1.1em;padding-bottom:5px;} + +.jsenabled .gradingform_guide .criterionnamelabel {display: none;} +.jsenabled .gradingform_guide .criterionshortname {font-weight:bold;} +.gradingform_guide table {width: 100%} +.gradingform_guide .criteriondescriptionmarkers {width: 300px;} +.gradingform_guide .markingguideremark {width: 100%;} +.gradingform_guide .criteriondescriptionscore {display: inline;} \ No newline at end of file diff --git a/grade/grading/form/guide/version.php b/grade/grading/form/guide/version.php new file mode 100644 index 0000000000000..91ca0ed49868f --- /dev/null +++ b/grade/grading/form/guide/version.php @@ -0,0 +1,30 @@ +. + +/** + * Marking guide, advanced grade plugin + * + * @package gradingform_guide + * @copyright 2012 Dan Marsden + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'gradingform_guide'; +$plugin->version = 2012022100; +$plugin->requires = 2011110200; +$plugin->maturity = MATURITY_STABLE; \ No newline at end of file diff --git a/lib/pluginlib.php b/lib/pluginlib.php index 150d42c69c6f5..b34264b711847 100644 --- a/lib/pluginlib.php +++ b/lib/pluginlib.php @@ -418,7 +418,7 @@ public static function standard_plugins_list($type) { ), 'gradingform' => array( - 'rubric' + 'rubric', 'guide' ), 'local' => array(