diff --git a/grade/grading/form/lib.php b/grade/grading/form/lib.php index 01de4a8f4bf..e6f2272a0a2 100644 --- a/grade/grading/form/lib.php +++ b/grade/grading/form/lib.php @@ -289,6 +289,18 @@ abstract class gradingform_controller { return true; } + /** + * Returns the renderer for the current plugin + * + * @param string $subtype optional subtype + * @param string $target one of rendering target constants + * @return renderer_base + */ + public function get_renderer($subtype = null, $target = null) { + global $PAGE; + return $PAGE->get_renderer('gradingform_'. $this->get_method_name(), $subtype, $target); + } + //////////////////////////////////////////////////////////////////////////// diff --git a/grade/grading/form/rubric/js/rubric.js b/grade/grading/form/rubric/js/rubric.js index c154d2563da..8c9f232cf98 100644 --- a/grade/grading/form/rubric/js/rubric.js +++ b/grade/grading/form/rubric/js/rubric.js @@ -6,6 +6,11 @@ M.gradingform_rubric = {}; M.gradingform_rubric.init = function(Y, options) { Y.on('click', M.gradingform_rubric.levelclick, '#rubric-'+options.name+' .level', null, Y, options.name); Y.all('#rubric-'+options.name+' .radio').setStyle('display', 'none') + Y.all('#rubric-'+options.name+' .level').each(function (node) { + if (node.one('input[type=radio][checked]')) { + node.addClass('checked'); + } + }); }; M.gradingform_rubric.levelclick = function(e, Y, name) { diff --git a/grade/grading/form/rubric/js/rubriceditor.js b/grade/grading/form/rubric/js/rubriceditor.js index 4ef9b15d44f..107348c9b86 100644 --- a/grade/grading/form/rubric/js/rubriceditor.js +++ b/grade/grading/form/rubric/js/rubriceditor.js @@ -8,13 +8,82 @@ M.gradingform_rubriceditor.init = function(Y, options) { 'criterion' : options.criteriontemplate, 'level' : options.leveltemplate } - M.gradingform_rubriceditor.addhandlers(Y, options.name); + M.gradingform_rubriceditor.disablealleditors(null, Y, options.name) + M.gradingform_rubriceditor.addhandlers(Y, options.name) }; // Adds handlers for clicking submit button. This function must be called each time JS adds new elements to html M.gradingform_rubriceditor.addhandlers = function(Y, name) { if (M.gradingform_rubriceditor.eventhandler) M.gradingform_rubriceditor.eventhandler.detach() - M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.buttonclick, '#rubriceditor-'+name+' input[type=submit]', null, Y, name); + M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.clickanywhere, 'body', null, Y, name); + M.gradingform_rubriceditor.eventhandler = Y.on('click', M.gradingform_rubriceditor.buttonclick, '#rubric-'+name+' input[type=submit]', null, Y, name); +} + +M.gradingform_rubriceditor.disablealleditors = function(e, Y, name) { + Y.all('#rubric-'+name+' .level').each( function(node) {M.gradingform_rubriceditor.editmode(node, false)} ); + Y.all('#rubric-'+name+' .description').each( function(node) {M.gradingform_rubriceditor.editmode(node, false)} ); +} + +M.gradingform_rubriceditor.clickanywhere = function(e, Y, name) { + var el = e.target + // if clicked on button - disablecurrenteditor, continue + if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') { + M.gradingform_rubriceditor.disablealleditors(null, Y, name) + return + } + // else if clicked on level and this level is not enabled - enable it + // or if clicked on description and this description is not enabled - enable it + while (el && !(el.hasClass('level') || el.hasClass('description'))) el = el.get('parentNode') + if (el) { + if (el.one('textarea').getStyle('display') == 'none') { + M.gradingform_rubriceditor.disablealleditors(null, Y, name) + M.gradingform_rubriceditor.editmode(el, true) + } + return + } + // else disablecurrenteditor + M.gradingform_rubriceditor.disablealleditors(null, Y, name) +} + +M.gradingform_rubriceditor.editmode = function(el, editmode) { + var ta = el.one('textarea') + if (!ta.get('parentNode').one('.plainvalue')) { + ta.get('parentNode').append('
') + } + var tb = el.one('input[type=text]') + if (tb && !tb.get('parentNode').one('.plainvalue')) { + tb.get('parentNode').append('') + } + if (!editmode) { + var value = ta.get('value') + if (value.length) ta.get('parentNode').one('.plainvalue').removeClass('empty') + else { + value = (el.hasClass('level')) ? M.str.gradingform_rubric.levelempty : M.str.gradingform_rubric.criterionempty + ta.get('parentNode').one('.plainvalue').addClass('empty') + } + ta.get('parentNode').one('.plainvalue').set('innerHTML', value) + ta.get('parentNode').one('.plainvalue').setStyle('display', 'block') + ta.setStyle('display', 'none') + if (tb) { + tb.get('parentNode').one('.plainvalue').set('innerHTML', tb.get('value')) + tb.get('parentNode').one('.plainvalue').setStyle('display', 'inline-block') + tb.setStyle('display', 'none') + } + } else { + if (tb) { + tb.get('parentNode').one('.plainvalue').setStyle('display', 'none') + tb.setStyle('display', 'inline-block') + } + var width = ta.get('parentNode').getComputedStyle('width') // TODO min width + var height = ta.get('parentNode').getComputedStyle('height') // TODO min height + if (el.hasClass('level')) { + height = el.getComputedStyle('height') - el.one('.score').getComputedStyle('height') + } else if (el.hasClass('description')) { + height = el.get('parentNode').getComputedStyle('height') + } + ta.get('parentNode').one('.plainvalue').setStyle('display', 'none') + ta.setStyle('display', 'block').setStyle('width', width).setStyle('height', height) + } } // handler for clicking on submit buttons within rubriceditor element. Adds/deletes/rearranges criteria and/or levels on client side @@ -25,9 +94,9 @@ M.gradingform_rubriceditor.buttonclick = function(e, Y, name, confirmed) { if (chunks[0] != name) return; var elements_str if (chunks.length>3 || action == 'addlevel') { - elements_str = '#rubriceditor-'+name+' #'+name+'-'+chunks[1]+'-levels .level' + elements_str = '#rubric-'+name+' #'+name+'-'+chunks[1]+'-levels .level' } else { - elements_str = '#rubriceditor-'+name+' .criterion' + elements_str = '#rubric-'+name+' .criterion' } // prepare the id of the next inserted level or criterion var newid = 1 @@ -48,7 +117,7 @@ M.gradingform_rubriceditor.buttonclick = function(e, Y, name, confirmed) { replace(/\{CRITERION-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '') Y.one('#'+name+'-criteria').append(newcriterion) M.gradingform_rubriceditor.addhandlers(Y, name); - } else if (chunks.length == 3 && action == 'addlevel') { + } else if (chunks.length == 4 && action == 'addlevel') { // ADD NEW LEVEL var newlevel = M.gradingform_rubriceditor.templates[name]['level']. replace(/\{CRITERION-id\}/g, chunks[1]).replace(/\{LEVEL-id\}/g, 'NEWID'+newid).replace(/\{.+?\}/g, '') diff --git a/grade/grading/form/rubric/lang/en/gradingform_rubric.php b/grade/grading/form/rubric/lang/en/gradingform_rubric.php index 3c4ee9ef1ad..a634486d330 100644 --- a/grade/grading/form/rubric/lang/en/gradingform_rubric.php +++ b/grade/grading/form/rubric/lang/en/gradingform_rubric.php @@ -33,9 +33,12 @@ $string['description'] = 'Description'; $string['name'] = 'Name'; $string['addcriterion'] = 'Add criterion'; -$string['criterionmoveup'] = 'Up'; -$string['criteriondelete'] = 'Delete'; -$string['criterionmovedown'] = 'Down'; +$string['criterionmoveup'] = 'Move up'; +$string['criteriondelete'] = 'Delete criterion'; +$string['criterionmovedown'] = 'Move down'; $string['criterionaddlevel'] = 'Add level'; $string['scorepostfix'] = ' pts'; -$string['leveldelete'] = 'Del'; +$string['leveldelete'] = 'Delete level'; + +$string['criterionempty'] = 'Click to edit criterion'; +$string['levelempty'] = 'Click to edit level'; diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php index 74c56a79fbf..0573eac2048 100644 --- a/grade/grading/form/rubric/lib.php +++ b/grade/grading/form/rubric/lib.php @@ -32,6 +32,13 @@ require_once($CFG->dirroot.'/grade/grading/form/lib.php'); * This controller encapsulates the rubric grading logic */ class gradingform_rubric_controller extends gradingform_controller { + // Modes of displaying the rubric (used in gradingform_rubric_renderer) + const DISPLAY_EDIT_FULL = 1; // For editing (moderator or teacher creates a rubric) + const DISPLAY_EDIT_FROZEN = 2; // Preview the rubric design with hidden fields + const DISPLAY_PREVIEW = 3; // Preview the rubric design + const DISPLAY_EVAL = 4; // For evaluation, enabled (teacher grades a student) + const DISPLAY_EVAL_FROZEN = 5; // For evaluation, with hidden fields + const DISPLAY_REVIEW = 6; // Dispaly filled rubric (i.e. students see their grades) /** * Extends the module settings navigation with the rubric grading settings @@ -306,6 +313,32 @@ class gradingform_rubric_controller extends gradingform_controller { * Returns html for form element */ public function to_html($gradingformelement) { + global $PAGE, $USER; + if (!$gradingformelement->_flagFrozen) { + $module = array('name'=>'gradingform_rubric', 'fullpath'=>'/grade/grading/form/rubric/js/rubric.js'); + $PAGE->requires->js_init_call('M.gradingform_rubric.init', array(array('name' => $gradingformelement->getName(), 'criteriontemplate' =>'', 'leveltemplate' => '')), true, $module); + $mode = self::DISPLAY_EVAL; + } else { + if ($this->_persistantFreeze) { + $mode = gradingform_rubric_controller::DISPLAY_EVAL_FROZEN; + } else { + $mode = gradingform_rubric_controller::DISPLAY_REVIEW; + } + } + $criteria = $this->definition->rubric_criteria; + $submissionid = $gradingformelement->get_grading_attribute('submissionid'); + $raterid = $USER->id; // TODO - this is very strange! + $value = $gradingformelement->getValue(); + if ($value === null) { + $value = $this->get_grading($raterid, $submissionid); // TODO maybe implement in form->set_data() ? + } + return $this->get_renderer()->display_rubric($criteria, $mode, $gradingformelement->getName(), $value); + } + + /** + * Returns html for form element + */ + public function to_html_old($gradingformelement) { global $PAGE, $USER; //TODO move to renderer diff --git a/grade/grading/form/rubric/renderer.php b/grade/grading/form/rubric/renderer.php index 039823df1e4..349eb3c85b1 100644 --- a/grade/grading/form/rubric/renderer.php +++ b/grade/grading/form/rubric/renderer.php @@ -28,7 +28,7 @@ defined('MOODLE_INTERNAL') || die(); /** * Grading method plugin renderer */ -class gradingform_rubric_renderer extends gradingform_renderer { +class gradingform_rubric_renderer { /** * Renders grading widget @@ -43,4 +43,188 @@ class gradingform_rubric_renderer extends gradingform_renderer { return $this->output->container($button.$span, 'gradingform_rubric-widget-wrapper', 1); } + + /** + * + * @param int $mode @see gradingform_rubric_controller + * @return string + */ + public function criterion_template($mode, $elementname = '{NAME}', $criterion = null, $levels_str = '{LEVELS}') { + // TODO description format + if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) { + $criterion = array('id' => '{CRITERION-id}', 'description' => '{CRITERION-description}', 'sortorder' => '{CRITERION-sortorder}', 'class' => '{CRITERION-class}'); + } else { + foreach (array('sortorder', 'description', 'class') as $key) { + // set missing array elements to empty strings to avoid warnings + if (!array_key_exists($key, $criterion)) { + $criterion[$key] = ''; + } + } + } + $criterion_template = html_writer::start_tag('div', array('class' => 'clearfix criterion'. $criterion['class'], 'id' => '{NAME}-{CRITERION-id}')); + if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { + $criterion_template .= html_writer::start_tag('div', array('class' => 'controls')); + foreach (array('moveup', 'delete', 'movedown') as $key) { + $value = get_string('criterion'.$key, 'gradingform_rubric'); + $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[{CRITERION-id}]['.$key.']', + 'id' => '{NAME}-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value)); + $criterion_template .= html_writer::tag('div', $button, array('class' => $key)); + } + $criterion_template .= html_writer::end_tag('div'); // .controls + $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder'])); + $description = html_writer::tag('textarea', htmlspecialchars($criterion['description']), array('name' => '{NAME}[{CRITERION-id}][description]', 'cols' => '10', 'rows' => '5')); + } else { + if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) { + $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder'])); + $criterion_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][description]', 'value' => $criterion['description'])); + } + $description = $criterion['description']; + } + $criterion_template .= html_writer::tag('div', $description, array('class' => 'description', 'id' => '{NAME}-{CRITERION-id}-description')); + $criterion_template .= html_writer::tag('div', $levels_str, array('class' => 'clearfix levels', 'id' => '{NAME}-{CRITERION-id}-levels')); + if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { + $value = get_string('criterionaddlevel', 'gradingform_rubric'); + $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[{CRITERION-id}][levels][addlevel]', + 'id' => '{NAME}-{CRITERION-id}-levels-addlevel', 'value' => $value, 'title' => $value)); //TODO '{NAME}-{CRITERION-id}-levels-addlevel + $criterion_template .= html_writer::tag('div', $button, array('class' => 'addlevel')); + } + $criterion_template .= html_writer::end_tag('div'); // .criterion + + $criterion_template = str_replace('{NAME}', $elementname, $criterion_template); + $criterion_template = str_replace('{CRITERION-id}', $criterion['id'], $criterion_template); + return $criterion_template; + } + + public function level_template($mode, $elementname = '{NAME}', $criterionid = '{CRITERION-id}', $level = null) { + // TODO definition format + if ($level === null || !is_array($level) || !array_key_exists('id', $level)) { + $level = array('id' => '{LEVEL-id}', 'definition' => '{LEVEL-definition}', 'score' => '{LEVEL-score}', 'class' => '{LEVEL-class}', 'checked' => false); + } else { + foreach (array('score', 'definition', 'class', 'checked') as $key) { + // set missing array elements to empty strings to avoid warnings + if (!array_key_exists($key, $level)) { + $level[$key] = ''; + } + } + } + + // Template for one level within one criterion + $level_template = html_writer::start_tag('div', array('id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}', 'class' => 'clearfix level'. $level['class'])); + if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { + $definition = html_writer::tag('textarea', htmlspecialchars($level['definition']), array('name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][definition]', 'cols' => '10', 'rows' => '4')); + $score = html_writer::empty_tag('input', array('type' => 'text', 'name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][score]', 'size' => '4', 'value' => $level['score'])); + } else { + if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) { + $level_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition'])); + $level_template .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][score]', 'value' => $level['score'])); + } + $definition = $level['definition']; + $score = $level['score']; + } + if ($mode == gradingform_rubric_controller::DISPLAY_EVAL) { + $input = html_writer::empty_tag('input', array('type' => 'radio', 'name' => '{NAME}[{CRITERION-id}]', 'value' => $level['id']) + + ($level['checked'] ? array('checked' => 'checked') : array())); + $level_template .= html_writer::tag('div', $input, array('class' => 'radio')); + } + if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) { + $html .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[{CRITERION-id}]', 'value' => $level['id'])); + } + $score = html_writer::tag('span', $score, array('id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}-score')); + $level_template .= html_writer::tag('div', $definition, array('class' => 'definition', 'id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}-definition')); + $level_template .= html_writer::tag('div', $score. get_string('scorepostfix', 'gradingform_rubric'), array('class' => 'score')); + if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { + $value = get_string('leveldelete', 'gradingform_rubric'); + $button = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[{CRITERION-id}][levels][{LEVEL-id}][delete]', 'id' => '{NAME}-{CRITERION-id}-levels-{LEVEL-id}-delete', 'value' => $value, 'title' => $value)); + $level_template .= html_writer::tag('div', $button, array('class' => 'delete')); + } + $level_template .= html_writer::end_tag('div'); // .level + + $level_template = str_replace('{NAME}', $elementname, $level_template); + $level_template = str_replace('{CRITERION-id}', $criterionid, $level_template); + $level_template = str_replace('{LEVEL-id}', $level['id'], $level_template); + return $level_template; + } + + protected function rubric_template($mode, $elementname = '{NAME}', $criteria_str = '{CRITERIA}') { + $classsuffix = ''; // CSS suffix for class of the main div. Depends on the mode + switch ($mode) { + case gradingform_rubric_controller::DISPLAY_EDIT_FULL: + $classsuffix = ' editor editable'; break; + case gradingform_rubric_controller::DISPLAY_EDIT_FROZEN: + $classsuffix = ' editor frozen'; break; + case gradingform_rubric_controller::DISPLAY_PREVIEW: + $classsuffix = ' editor preview'; break; + case gradingform_rubric_controller::DISPLAY_EVAL: + $classsuffix = ' evaluate editable'; break; + case gradingform_rubric_controller::DISPLAY_EVAL_FROZEN: + $classsuffix = ' evaluate frozen'; break; + case gradingform_rubric_controller::DISPLAY_REVIEW: + $classsuffix = ' review'; break; + } + + $rubric_template = html_writer::start_tag('div', array('id' => 'rubric-{NAME}', 'class' => 'clearfix form_rubric'.$classsuffix)); + $rubric_template .= html_writer::tag('div', $criteria_str, array('class' => 'criteria', 'id' => '{NAME}-criteria')); + if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) { + $value = get_string('addcriterion', 'gradingform_rubric'); + $input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[addcriterion]', 'id' => '{NAME}-addcriterion', 'value' => $value, 'title' => $value)); + $rubric_template .= html_writer::tag('div', $input, array('class' => 'addcriterion')); + } + $rubric_template .= html_writer::end_tag('div'); + + return str_replace('{NAME}', $elementname, $rubric_template); + } + + /** + * Returns html code for displaying the rubric in the specified mode + * + * @param array $criteria + * @param int $mode + * @param string $elementname + * @param array $values + * @return string + */ + public function display_rubric($criteria, $mode, $elementname = null, $values = null) { + $criteria_str = ''; + $cnt = 0; + foreach ($criteria as $id => $criterion) { + $criterion['class'] = $this->get_css_class_suffix($cnt++, sizeof($criteria) -1); + $levels_str = ''; + $levelcnt = 0; + foreach ($criterion['levels'] as $levelid => $level) { + $level['score'] = (float)$level['score']; // otherwise the display will look like 1.00000 + $level['class'] = $this->get_css_class_suffix($levelcnt++, sizeof($criterion['levels']) -1); + $level['checked'] = (is_array($values) && (array_key_exists($id, $values) && ((int)$values[$id] === $levelid))); + if ($level['checked'] && ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN || $mode == gradingform_rubric_controller::DISPLAY_REVIEW)) { + $level['class'] .= ' checked'; + //in mode DISPLAY_EVAL the class 'checked' will be added by JS if it is enabled. If it is not enabled, the 'checked' class will only confuse + } + $levels_str .= $this->level_template($mode, $elementname, $id, $level); + } + $criteria_str .= $this->criterion_template($mode, $elementname, $criterion, $levels_str); + } + return $this->rubric_template($mode, $elementname, $criteria_str); + } + + /** + * Help function to return CSS class names for element (first/last/even/odd) + * + * @param