MDL-30270, MDL-30269: rubric interface/usability improvements:
authorMarina Glancy <marina@moodle.com>
Fri, 18 Nov 2011 07:35:24 +0000 (15:35 +0800)
committerMarina Glancy <marina@moodle.com>
Fri, 18 Nov 2011 07:35:24 +0000 (15:35 +0800)
- In rubric editor the line 'Current rubric status' is hidden if there is no status yet
- If present the style of the status is the same as on manage.php page
- For newly created rubric 'Add criterion' button is pre-pressed automatically
- Changed JavaScript to work for Mac browsers default settings and for IPad
- MDL-30269: added explanation message about score to grade mapping
- fixed bug with non-javascript 'Add criterion' behaviour

grade/grading/form/rubric/edit.php
grade/grading/form/rubric/edit_form.php
grade/grading/form/rubric/js/rubriceditor.js
grade/grading/form/rubric/lang/en/gradingform_rubric.php
grade/grading/form/rubric/lib.php
grade/grading/form/rubric/renderer.php
grade/grading/form/rubric/rubriceditor.php
grade/grading/form/rubric/styles.css

index d9390bb..09daca9 100644 (file)
@@ -44,8 +44,8 @@ $PAGE->set_url(new moodle_url('/grade/grading/form/rubric/edit.php', array('area
 $PAGE->set_title(get_string('definerubric', 'gradingform_rubric'));
 $PAGE->set_heading(get_string('definerubric', 'gradingform_rubric'));
 
-$mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'context' => $context, 'allowdraft' => !$controller->has_active_instances()));
-$data = $controller->get_definition_for_editing();
+$mform = new gradingform_rubric_editrubric(null, array('areaid' => $areaid, 'context' => $context, 'allowdraft' => !$controller->has_active_instances()), 'post', '', array('class' => 'gradingform_rubric_editform'));
+$data = $controller->get_definition_for_editing(true);
 $returnurl = optional_param('returnurl', $manager->get_management_url(), PARAM_LOCALURL);
 $data->returnurl = $returnurl;
 $mform->set_data($data);
index 3bae783..64cf8f9 100644 (file)
@@ -58,8 +58,8 @@ class gradingform_rubric_editrubric extends moodleform {
 
         // rubric completion status
         $choices = array();
-        $choices[gradingform_controller::DEFINITION_STATUS_DRAFT]    = get_string('statusdraft', 'grading');
-        $choices[gradingform_controller::DEFINITION_STATUS_READY]    = get_string('statusready', 'grading');
+        $choices[gradingform_controller::DEFINITION_STATUS_DRAFT]    = html_writer::tag('span', get_string('statusdraft', 'core_grading'), array('class' => 'status draft'));
+        $choices[gradingform_controller::DEFINITION_STATUS_READY]    = html_writer::tag('span', get_string('statusready', 'core_grading'), array('class' => 'status ready'));
         $form->addElement('select', 'status', get_string('rubricstatus', 'gradingform_rubric'), $choices)->freeze();
 
         // rubric editor
@@ -80,6 +80,27 @@ class gradingform_rubric_editrubric extends moodleform {
         $form->closeHeaderBefore('buttonar');
     }
 
+    /**
+     * Setup the form depending on current values. This method is called after definition(),
+     * data submission and set_data().
+     * All form setup that is dependent on form values should go in here.
+     *
+     * We remove the element status if there is no current status (i.e. rubric is only being created)
+     * so the users do not get confused
+     */
+    public function definition_after_data() {
+        $form = $this->_form;
+        $el = $form->getElement('status');
+        if (!$el->getValue()) {
+            $form->removeElement('status');
+        } else {
+            $vals = array_values($el->getValue());
+            if ($vals[0] == gradingform_controller::DEFINITION_STATUS_READY) {
+                $this->findButton('saverubric')->setValue(get_string('save', 'gradingform_rubric'));
+            }
+        }
+    }
+
     /**
      * Form vlidation.
      * If there are errors return array of errors ("fieldname"=>"error message"),
index de3dd19..1d5d524 100644 (file)
@@ -12,6 +12,10 @@ M.gradingform_rubriceditor.init = function(Y, options) {
     }
     M.gradingform_rubriceditor.disablealleditors()
     Y.on('click', M.gradingform_rubriceditor.clickanywhere, 'body', null)
+    YUI().use('event-touch', function (Y) {
+        Y.one('body').on('touchstart', M.gradingform_rubriceditor.clickanywhere);
+        Y.one('body').on('touchend', M.gradingform_rubriceditor.clickanywhere);
+    })
     M.gradingform_rubriceditor.addhandlers()
 };
 
@@ -35,6 +39,7 @@ M.gradingform_rubriceditor.disablealleditors = function() {
 // it switches this element to edit mode. If rubric button is clicked it does nothing so the 'buttonclick'
 // function is invoked
 M.gradingform_rubriceditor.clickanywhere = function(e) {
+    if (e.type == 'touchstart') return
     var el = e.target
     // if clicked on button - disablecurrenteditor, continue
     if (el.get('tagName') == 'INPUT' && el.get('type') == 'submit') {
@@ -48,7 +53,7 @@ M.gradingform_rubriceditor.clickanywhere = function(e) {
         el = el.get('parentNode')
     }
     if (el) {
-        if (el.one('textarea').getStyle('display') == 'none') {
+        if (el.one('textarea').hasClass('hiddenelement')) {
             M.gradingform_rubriceditor.disablealleditors()
             M.gradingform_rubriceditor.editmode(el, true, focustb)
         }
@@ -61,19 +66,19 @@ M.gradingform_rubriceditor.clickanywhere = function(e) {
 // switch the criterion description or level to edit mode or switch back
 M.gradingform_rubriceditor.editmode = function(el, editmode, focustb) {
     var ta = el.one('textarea')
-    if (!editmode && ta.getStyle('display') == 'none') return;
-    if (editmode && ta.getStyle('display') == 'block') return;
-    var pseudotablink = '<a href="#" class="pseudotablink">&nbsp;</a>',
+    if (!editmode && ta.hasClass('hiddenelement')) return;
+    if (editmode && !ta.hasClass('hiddenelement')) return;
+    var pseudotablink = '<input type="text" size="1" class="pseudotablink"/>',
         taplain = ta.get('parentNode').one('.plainvalue'),
         tbplain = null,
-        tb = el.one('input[type=text]')
+        tb = el.one('.score input[type=text]')
     // add 'plainvalue' next to textarea for description/definition and next to input text field for score (if applicable)
     if (!taplain) {
-        ta.get('parentNode').append('<div class="plainvalue"><span class="textvalue">&nbsp;</span>'+pseudotablink+'</div>')
+        ta.get('parentNode').append('<div class="plainvalue">'+pseudotablink+'<span class="textvalue">&nbsp;</span></div>')
         taplain = ta.get('parentNode').one('.plainvalue')
         taplain.one('.pseudotablink').on('focus', M.gradingform_rubriceditor.clickanywhere)
         if (tb) {
-            tb.get('parentNode').append('<div class="plainvalue"><span class="textvalue">&nbsp;</span>'+pseudotablink+'</div>')
+            tb.get('parentNode').append('<span class="plainvalue">'+pseudotablink+'<span class="textvalue">&nbsp;</span></span>')
             tbplain = tb.get('parentNode').one('.plainvalue')
             tbplain.one('.pseudotablink').on('focus', M.gradingform_rubriceditor.clickanywhere)
         }
@@ -90,20 +95,33 @@ M.gradingform_rubriceditor.editmode = function(el, editmode, focustb) {
         }
         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
-        var width = parseFloat(ta.get('parentNode').getComputedStyle('width')),
-            height
-        if (el.hasClass('level')) height = parseFloat(el.getComputedStyle('height')) - parseFloat(el.one('.score').getComputedStyle('height'))
-        else height = parseFloat(ta.get('parentNode').getComputedStyle('height'))
-        ta.setStyle('width', Math.max(width,50)+'px').setStyle('height', Math.max(height,20)+'px')
-    }
-    // hide/display textarea, textbox and plaintexts
-    taplain.setStyle('display', editmode ? 'none' : 'block')
-    ta.setStyle('display', editmode ? 'block' : 'none')
-    if (tb) {
-        tbplain.setStyle('display', editmode ? 'none' : 'inline-block')
-        tb.setStyle('display', editmode ? 'inline-block' : 'none')
+        try {
+            var width = parseFloat(ta.get('parentNode').getComputedStyle('width')),
+                height
+            if (el.hasClass('level')) height = parseFloat(el.getComputedStyle('height')) - parseFloat(el.one('.score').getComputedStyle('height'))
+            else height = parseFloat(ta.get('parentNode').getComputedStyle('height'))
+            ta.setStyle('width', Math.max(width,50)+'px')
+            ta.setStyle('height', Math.max(height,20)+'px')
+        }
+        catch (err) {
+            // this browser do not support 'computedStyle', leave the default size of the textbox
+        }
+        // hide/display textarea, textbox and plaintexts
+        taplain.addClass('hiddenelement')
+        ta.removeClass('hiddenelement')
+        if (tb) {
+            tbplain.addClass('hiddenelement')
+            tb.removeClass('hiddenelement')
+        }
     }
     // focus the proper input field in edit mode
     if (editmode) { if (tb && focustb) tb.focus(); else ta.focus() }
index 0d78aa6..d7f605c 100644 (file)
@@ -53,12 +53,18 @@ $string['regradeoption0'] = 'Do not mark for regrade';
 $string['regradeoption1'] = 'Mark for regrade';
 $string['restoredfromdraft'] = 'NOTE: The last attempt to grade this person was not saved properly so draft grades have been restored. If you want to cancel these changes use the \'Cancel\' button below.';
 $string['rubric'] = 'Rubric';
+$string['rubricmapping'] = 'Score to grade mapping rules';
+$string['rubricmappingexplained'] = 'The minimum possible score for this rubric is <b>{$a->minscore} points</b> and it will be converted to the minimum grade available in this module (which is zero unless the scale is used).
+    The maximum score <b>{$a->maxscore} points</b> will be converted to the maximum grade.<br />
+    Intermediate scores will be converted respectively and rounded to the nearest available grade.<br />
+    If a scale is used instead of a grade, the score will be converted to the scale elements as if they were consecutive integers.';
 $string['rubricnotcompleted'] = 'Please choose something for each criterion';
 $string['rubricoptions'] = 'Rubric options';
 $string['rubricstatus'] = 'Current rubric status';
+$string['save'] = 'Save';
 $string['saverubric'] = 'Save rubric and make it ready';
 $string['saverubricdraft'] = 'Save as draft';
-$string['scorepostfix'] = '{$a} points';
+$string['scorepostfix'] = '{$a}points';
 $string['showdescriptionstudent'] = 'Display rubric description to those being graded';
 $string['showdescriptionteacher'] = 'Display rubric description during evaluation';
 $string['showremarksstudent'] = 'Show remarks to those being graded';
@@ -66,6 +72,4 @@ $string['showscorestudent'] = 'Display points for each level to those being grad
 $string['showscoreteacher'] = 'Display points for each level during evaluation';
 $string['sortlevelsasc'] = 'Sort order for levels:';
 $string['sortlevelsasc0'] = 'Descending by number of points';
-$string['sortlevelsasc1'] = 'Ascending by number of points';
-$string['statusdraft'] = 'Draft';
-$string['statusready'] = 'Ready';
\ No newline at end of file
+$string['sortlevelsasc1'] = 'Ascending by number of points';
\ No newline at end of file
index 6aa0d98..ff24e85 100644 (file)
@@ -360,9 +360,10 @@ class gradingform_rubric_controller extends gradingform_controller {
     /**
      * Converts the current definition into an object suitable for the editor form's set_data()
      *
+     * @param boolean $addemptycriterion whether to add an empty criterion if the rubric is completely empty (just being created)
      * @return stdClass
      */
-    public function get_definition_for_editing() {
+    public function get_definition_for_editing($addemptycriterion = false) {
 
         $definition = $this->get_definition();
         $properties = new stdClass();
@@ -378,6 +379,8 @@ class gradingform_rubric_controller extends gradingform_controller {
         $properties->rubric = array('criteria' => array(), 'options' => $this->get_options());
         if (!empty($definition->rubric_criteria)) {
             $properties->rubric['criteria'] = $definition->rubric_criteria;
+        } else if (!$definition && $addemptycriterion) {
+            $properties->rubric['criteria'] = array('addcriterion' => 1);
         }
 
         return $properties;
@@ -481,7 +484,8 @@ class gradingform_rubric_controller extends gradingform_controller {
         $output = $this->get_renderer($page);
         $criteria = $this->definition->rubric_criteria;
         $options = $this->get_options();
-        $rubric = $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric');
+        $rubric = $output->display_rubric_mapping_explained($this->get_min_max_score());
+        $rubric .= $output->display_rubric($criteria, $options, self::DISPLAY_PREVIEW, 'rubric');
 
         return $rubric;
     }
@@ -590,6 +594,28 @@ class gradingform_rubric_controller extends gradingform_controller {
 
         return array($subsql, $params);
     }
+
+    /**
+     * Calculates and returns the possible minimum and maximum score (in points) for this rubric
+     *
+     * @return array
+     */
+    public function get_min_max_score() {
+        if (!$this->is_form_available()) {
+            return null;
+        }
+        $returnvalue = array('minscore' => 0, 'maxscore' => 0);
+        foreach ($this->get_definition()->rubric_criteria as $id => $criterion) {
+            $scores = array();
+            foreach ($criterion['levels'] as $level) {
+                $scores[] = $level['score'];
+            }
+            sort($scores);
+            $returnvalue['minscore'] += $scores[0];
+            $returnvalue['maxscore'] += $scores[sizeof($scores)-1];
+        }
+        return $returnvalue;
+    }
 }
 
 /**
@@ -717,19 +743,7 @@ class gradingform_rubric_instance extends gradingform_instance {
         global $DB, $USER;
         $grade = $this->get_rubric_filling();
 
-        $minscore = 0;
-        $maxscore = 0;
-        foreach ($this->get_controller()->get_definition()->rubric_criteria as $id => $criterion) {
-            $scores = array();
-            foreach ($criterion['levels'] as $level) {
-                $scores[] = $level['score'];
-            }
-            sort($scores);
-            $minscore += $scores[0];
-            $maxscore += $scores[sizeof($scores)-1];
-        }
-
-        if ($maxscore <= $minscore) {
+        if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) {
             return -1;
         }
 
@@ -745,7 +759,7 @@ class gradingform_rubric_instance extends gradingform_instance {
         foreach ($grade['criteria'] as $id => $record) {
             $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score'];
         }
-        return round(($curscore-$minscore)/($maxscore-$minscore)*($maxgrade-$mingrade), 0) + $mingrade; // TODO mapping
+        return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade), 0) + $mingrade;
     }
 
     /**
index 8086833..f037263 100644 (file)
@@ -164,7 +164,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         $leveltemplate .= html_writer::start_tag('div', array('class' => 'level-wrapper'));
         if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FULL) {
             $definition = html_writer::tag('textarea', htmlspecialchars($level['definition']), array('name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'cols' => '10', 'rows' => '4'));
-            $score = html_writer::empty_tag('input', array('type' => 'text', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'size' => '4', 'value' => $level['score']));
+            $score = html_writer::empty_tag('input', array('type' => 'text', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][score]', 'size' => '3', 'value' => $level['score']));
         } else {
             if ($mode == gradingform_rubric_controller::DISPLAY_EDIT_FROZEN) {
                 $leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levels][{LEVEL-id}][definition]', 'value' => $level['definition']));
@@ -181,7 +181,7 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         if ($mode == gradingform_rubric_controller::DISPLAY_EVAL_FROZEN && $level['checked']) {
             $leveltemplate .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => '{NAME}[criteria][{CRITERION-id}][levelid]', 'value' => $level['id']));
         }
-        $score = html_writer::tag('span', $score, array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-score'));
+        $score = html_writer::tag('span', $score, array('id' => '{NAME}-criteria-{CRITERION-id}-levels-{LEVEL-id}-score', 'class' => 'scorevalue'));
         $definitionclass = 'definition';
         if (isset($level['error_definition'])) {
             $definitionclass .= ' error';
@@ -453,4 +453,22 @@ class gradingform_rubric_renderer extends plugin_renderer_base {
         $html .= html_writer::end_tag('div');
         return $html;
     }
+
+    /**
+     * Generates and returns HTML code to display information box about how rubric score is converted to the grade
+     *
+     * @param array $scores
+     * @return string
+     */
+    public function display_rubric_mapping_explained($scores) {
+        $html = '';
+        if (!$scores) {
+            return $html;
+        }
+        $html .= $this->box(
+                html_writer::tag('h4', get_string('rubricmapping', 'gradingform_rubric')).
+                html_writer::tag('div', get_string('rubricmappingexplained', 'gradingform_rubric', (object)$scores))
+                , 'generalbox rubricmappingexplained');
+        return $html;
+    }
 }
index 7fb2331..92cd3be 100644 (file)
@@ -184,14 +184,14 @@ class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
                 // when adding new criterion copy the number of levels and their scores from the last criterion
                 if (!empty($value['criteria'][$lastid]['levels'])) {
                     foreach ($value['criteria'][$lastid]['levels'] as $lastlevel) {
-                        $criterion['levels']['NEWID'+($i++)]['score'] = $lastlevel['score'];
+                        $criterion['levels']['NEWID'.($i++)]['score'] = $lastlevel['score'];
                     }
                 } else {
-                    $criterion['levels']['NEWID'+($i++)]['score'] = 0;
+                    $criterion['levels']['NEWID'.($i++)]['score'] = 0;
                 }
                 // add more levels so there are at least 3 in the new criterion. Increment by 1 the score for each next one
-                for ($i; $i<3; $i++) {
-                    $criterion['levels']['NEWID'+$i]['score'] = $criterion['levels']['NEWID'+($i-1)]['score'] + 1;
+                for ($i=$i; $i<3; $i++) {
+                    $criterion['levels']['NEWID'.$i]['score'] = $criterion['levels']['NEWID'.($i-1)]['score'] + 1;
                 }
                 // set other necessary fields (definition) for the levels in the new criterion
                 foreach (array_keys($criterion['levels']) as $i) {
index 4428ac3..41a5d10 100644 (file)
 
 */
 
+.gradingform_rubric_editform .status {font-weight:normal;text-transform:uppercase;font-size:60%;padding:0.25em;border:1px solid #EEE;-moz-border-radius:5px;}
+.gradingform_rubric_editform .status.ready {background-color:#e7f1c3;border-color:#AAEEAA;}
+.gradingform_rubric_editform .status.draft {background-color:#f3f2aa;border-color:#EEEE22;}
+
 .gradingform_rubric.editor .criterion .controls,
 .gradingform_rubric .criterion .description,
 .gradingform_rubric .criterion .levels,
@@ -75,7 +79,8 @@
 .gradingform_rubric .plainvalue.empty {font-style: italic; color: #AAA;}
 
 .gradingform_rubric.editor .criterion .levels .level .delete {position:absolute;right:0;bottom:0;}
-.gradingform_rubric .criterion .levels .level .score {font-style:italic;color:#575;font-weight: bold;margin-top:5px;}
+.gradingform_rubric .criterion .levels .level .score {font-style:italic;color:#575;font-weight: bold;margin-top:5px;nowrap:nowrap;}
+.gradingform_rubric .criterion .levels .level .score .scorevalue {padding-right:5px;}
 
 /* Make invisible the buttons 'Move up' for the first criterion and 'Move down' for the last, because those buttons will make no change */
 .gradingform_rubric.editor .criterion.first .controls .moveup input,
 .gradingform_rubric .criterion .levels .level .definition.error,
 .gradingform_rubric .criterion .levels .level .score.error {background:#FFDDDD;}
 
+/* special classes for elements created by rubriceditor.js */
+.gradingform_rubric.editor .hiddenelement {display:none;}
+.gradingform_rubric.editor .pseudotablink {background-color:transparent;border:0px solid;height:1px;width:1px;color:transparent;padding:0;margin:0;position:relative;float:right;}
+
 /**
  *
  */