Merge branch 'MDL-36606-master' of git://github.com/merrill-oakland/moodle
authorDan Poltawski <dan@moodle.com>
Mon, 12 Oct 2015 11:41:03 +0000 (12:41 +0100)
committerDan Poltawski <dan@moodle.com>
Mon, 12 Oct 2015 11:41:03 +0000 (12:41 +0100)
grade/report/grader/lib.php
grade/report/grader/module.js
grade/report/grader/tests/behat/ajax_grader.feature [new file with mode: 0644]
grade/report/grader/tests/behat/behat_gradereport_grader.php [new file with mode: 0644]
grade/tests/behat/behat_grade.php

index 88742ea..85d3a95 100644 (file)
@@ -746,7 +746,8 @@ class grade_report_grader extends grade_report {
             'cfg'       => array('ajaxenabled'=>false),
             'items'     => array(),
             'users'     => array(),
-            'feedback'  => array()
+            'feedback'  => array(),
+            'grades'    => array()
         );
         $jsscales = array();
 
@@ -1000,6 +1001,14 @@ class grade_report_grader extends grade_report {
 
                 } else if ($USER->gradeediting[$this->courseid]) {
 
+                    if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
+                        $itemcell->attributes['class'] .= ' grade_type_scale';
+                    } else if ($item->gradetype == GRADE_TYPE_VALUE) {
+                        $itemcell->attributes['class'] .= ' grade_type_value';
+                    } else if ($item->gradetype == GRADE_TYPE_TEXT) {
+                        $itemcell->attributes['class'] .= ' grade_type_text';
+                    }
+
                     if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
                         $scale = $scalesarray[$item->scaleid];
                         $gradeval = (int)$gradeval; // scales use only integers
@@ -1069,16 +1078,16 @@ class grade_report_grader extends grade_report {
 
                     if ($item->scaleid && !empty($scalesarray[$item->scaleid])) {
                         $itemcell->attributes['class'] .= ' grade_type_scale';
-                    } else if ($item->gradetype != GRADE_TYPE_TEXT) {
+                    } else if ($item->gradetype == GRADE_TYPE_VALUE) {
+                        $itemcell->attributes['class'] .= ' grade_type_value';
+                    } else if ($item->gradetype == GRADE_TYPE_TEXT) {
                         $itemcell->attributes['class'] .= ' grade_type_text';
                     }
 
-                    if ($enableajax) {
-                        $canoverride = true;
-                        if ($item->is_category_item() || $item->is_course_item()) {
-                            $canoverride = (bool) get_config('moodle', 'grade_overridecat');
-                        }
-                        if ($canoverride) {
+                    // Only allow edting if the grade is editable (not locked, not in a unoverridable category, etc).
+                    if ($enableajax && $grade->is_editable()) {
+                        // If a grade item is type text, and we don't have show quick feedback on, it can't be edited.
+                        if ($item->gradetype != GRADE_TYPE_TEXT || $showquickfeedback) {
                             $itemcell->attributes['class'] .= ' clickable';
                         }
                     }
@@ -1113,7 +1122,8 @@ class grade_report_grader extends grade_report {
             $jsarguments['cfg']['ajaxenabled'] = true;
             $jsarguments['cfg']['scales'] = array();
             foreach ($jsscales as $scale) {
-                $jsarguments['cfg']['scales'][$scale->id] = explode(',', $scale->scale);
+                // Trim the scale values, as they may have a space that is ommitted from values later.
+                $jsarguments['cfg']['scales'][$scale->id] = array_map('trim', explode(',', $scale->scale));
             }
             $jsarguments['cfg']['feedbacktrunclength'] =  $this->feedback_trunc_length;
 
index 1f4a073..2fac542 100644 (file)
@@ -209,9 +209,11 @@ M.gradereport_grader.classes.ajax.prototype.make_editable = function(e) {
     }
 
     // Sort out the field type
-    var fieldtype = 'text';
+    var fieldtype = 'value';
     if (node.hasClass('grade_type_scale')) {
         fieldtype = 'scale';
+    } else if (node.hasClass('grade_type_text')) {
+        fieldtype = 'text';
     }
     // Create the appropriate field widget
     switch (fieldtype) {
@@ -219,6 +221,8 @@ M.gradereport_grader.classes.ajax.prototype.make_editable = function(e) {
             this.current = new M.gradereport_grader.classes.scalefield(this.report, node);
             break;
         case 'text':
+            this.current = new M.gradereport_grader.classes.feedbackfield(this.report, node);
+            break;
         default:
             this.current = new M.gradereport_grader.classes.textfield(this.report, node);
             break;
@@ -322,7 +326,11 @@ M.gradereport_grader.classes.ajax.prototype.get_next_cell = function(cell) {
         next = tr.all('.grade').item(0);
     }
     if (!next) {
-        next = this.current.node;
+        return this.current.node;
+    }
+    // Continue on until we find a clickable cell
+    if (!next.hasClass('clickable')) {
+        return this.get_next_cell(next);
     }
     return next;
 };
@@ -342,7 +350,11 @@ M.gradereport_grader.classes.ajax.prototype.get_prev_cell = function(cell) {
         next = cells.item(cells.size()-1);
     }
     if (!next) {
-        next = this.current.node;
+        return this.current.node;
+    }
+    // Continue on until we find a clickable cell
+    if (!next.hasClass('clickable')) {
+        return this.get_prev_cell(next);
     }
     return next;
 };
@@ -366,7 +378,11 @@ M.gradereport_grader.classes.ajax.prototype.get_above_cell = function(cell) {
         next = tr.all('td.cell').item(column);
     }
     if (!next) {
-        next = this.current.node;
+        return this.current.node;
+    }
+    // Continue on until we find a clickable cell
+    if (!next.hasClass('clickable')) {
+        return this.get_above_cell(next);
     }
     return next;
 };
@@ -389,7 +405,13 @@ M.gradereport_grader.classes.ajax.prototype.get_below_cell = function(cell) {
         }
         next = tr.all('td.cell').item(column);
     }
-    // next will be null when we get to the bottom of a column
+    if (!next) {
+        return this.current.node;
+    }
+    // Continue on until we find a clickable cell
+    if (!next.hasClass('clickable')) {
+        return this.get_below_cell(next);
+    }
     return next;
 };
 /**
@@ -478,6 +500,7 @@ M.gradereport_grader.classes.ajax.prototype.submission_outcome = function(tid, o
                 }
                 // Calculate the final grade for the cell
                 var finalgrade = '';
+                var scalegrade = -1;
                 if (!r.finalgrade) {
                     if (this.report.isediting) {
                         // In edit mode don't put hyphens in the grade text boxes
@@ -488,35 +511,51 @@ M.gradereport_grader.classes.ajax.prototype.submission_outcome = function(tid, o
                     }
                 } else {
                     if (r.scale) {
-                        finalgrade = this.scales[r.scale][parseFloat(r.finalgrade)-1];
+                        scalegrade = parseFloat(r.finalgrade);
+                        finalgrade = this.scales[r.scale][scalegrade-1];
                     } else {
                         finalgrade = parseFloat(r.finalgrade).toFixed(info.itemdp);
                     }
                 }
                 if (this.report.isediting) {
-                    if (args.properties.itemtype == 'scale') {
-                        info.cell.one('#grade_'+r.userid+'_'+r.itemid).all('options').each(function(option){
-                            if (option.get('value') == finalgrade) {
-                                option.setAttribute('selected', 'selected');
-                            } else {
-                                option.removeAttribute('selected');
-                            }
-                        });
-                    } else {
-                        info.cell.one('#grade_'+r.userid+'_'+r.itemid).set('value', finalgrade);
+                    var grade = info.cell.one('#grade_'+r.userid+'_'+r.itemid);
+                    if (grade) {
+                        // This means the item has a input element to update.
+                        var parent = grade.ancestor('td');
+                        if (parent.hasClass('grade_type_scale')) {
+                            grade.all('option').each(function(option) {
+                                if (option.get('value') == scalegrade) {
+                                    option.setAttribute('selected', 'selected');
+                                } else {
+                                    option.removeAttribute('selected');
+                                }
+                            });
+                        } else {
+                            grade.set('value', finalgrade);
+                        }
+                    } else if (info.cell.one('.gradevalue')) {
+                        // This means we are updating a value for something without editing boxed (locked, etc).
+                        info.cell.one('.gradevalue').set('innerHTML', finalgrade);
                     }
                 } else {
                     // If there is no currently editing field or if this cell is not being currently edited
                     if (!this.current || info.cell.get('id') != this.current.node.get('id')) {
                         // Update the value
-                        info.cell.one('.gradevalue').set('innerHTML',finalgrade);
+                        var node = info.cell.one('.gradevalue');
+                        var td = node.ancestor('td');
+                        // Only scale and value type grades should have their content updated in this way.
+                        if (td.hasClass('grade_type_value') || td.hasClass('grade_type_scale')) {
+                            node.set('innerHTML', finalgrade);
+                        }
                     } else if (this.current && info.cell.get('id') == this.current.node.get('id')) {
                         // If we are here the grade value of the cell currently being edited has changed !!!!!!!!!
                         // If the user has not actually changed the old value yet we will automatically correct it
                         // otherwise we will prompt the user to choose to use their value or the new value!
                         if (!this.current.has_changed() || confirm(M.util.get_string('ajaxfieldchanged', 'gradereport_grader'))) {
                             this.current.set_grade(finalgrade);
-                            this.current.grade.set('value', finalgrade);
+                            if (this.current.grade) {
+                                this.current.grade.set('value', finalgrade);
+                            }
                         }
                     }
                 }
@@ -594,57 +633,81 @@ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
     this.editfeedback = ajax.showquickfeedback;
     this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid);
 
-    if (this.report.grades) {
-        for (var i = 0; i < this.report.grades.length; i++) {
-            if (this.report.grades[i]['user']==this.userid && this.report.grades[i]['item']==this.itemid) {
+    var i = 0;
+    if (this.grade) {
+        for (i = 0; i < this.report.grades.length; i++) {
+            if (this.report.grades[i]['user'] == this.userid && this.report.grades[i]['item'] == this.itemid) {
                 this.oldgrade = this.report.grades[i]['grade'];
             }
         }
-    }
 
-    if (!this.oldgrade) {
-        // Assigning an empty string makes determining whether the grade has been changed easier
-        // This value is never sent to the server
-        this.oldgrade = '';
-    }
+        if (!this.oldgrade) {
+            // Assigning an empty string makes determining whether the grade has been changed easier
+            // This value is never sent to the server
+            this.oldgrade = '';
+        }
 
-    // On blur save any changes in the grade field
-    this.grade.on('blur', this.submit, this);
+        // On blur save any changes in the grade field
+        this.grade.on('blur', this.submit, this);
+    }
 
     // Check if feedback is enabled
     if (this.editfeedback) {
         // Get the feedback fields
         this.feedback = this.report.Y.one('#feedback_'+userid+'_'+itemid);
 
-        for(var i = 0; i < this.report.feedback.length; i++) {
-            if (this.report.feedback[i]['user']==this.userid && this.report.feedback[i]['item']==this.itemid) {
-                this.oldfeedback = this.report.feedback[i]['content'];
+        if (this.feedback) {
+            for(i = 0; i < this.report.feedback.length; i++) {
+                if (this.report.feedback[i]['user'] == this.userid && this.report.feedback[i]['item'] == this.itemid) {
+                    this.oldfeedback = this.report.feedback[i]['content'];
+                }
             }
-        }
 
-        if(!this.oldfeedback) {
-            // Assigning an empty string makes determining whether the feedback has been changed easier
-            // This value is never sent to the server
-            this.oldfeedback = '';
-        }
+            if(!this.oldfeedback) {
+                // Assigning an empty string makes determining whether the feedback has been changed easier
+                // This value is never sent to the server
+                this.oldfeedback = '';
+            }
 
-        // On blur save any changes in the feedback field
-        this.feedback.on('blur', this.submit, this);
+            // On blur save any changes in the feedback field
+            this.feedback.on('blur', this.submit, this);
 
-        // Override the default tab movements when moving between cells
-        this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9+shift', this));                // Handle Shift+Tab
-        this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.feedback, 'press:9', this, true));                   // Handle Tab
-        this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.feedback, 'press:13', this));                // Handle the Enter key being pressed
-        this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.feedback, 'press:37,38,39,40+ctrl', this)); // Handle CTRL + arrow keys
+            // Override the default tab movements when moving between cells
+            // Handle Tab.
+            this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.feedback, 'press:9', this, true));
+            // Handle the Enter key being pressed.
+            this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.feedback, 'press:13', this));
+            // Handle CTRL + arrow keys.
+            this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.feedback, 'press:37,38,39,40+ctrl', this));
 
-        // Override the default tab movements for fields in the same cell
-        this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();this.grade.focus();}, this.feedback, 'press:9+shift', this));
-        this.keyevents.push(this.report.Y.on('key', function(e){if (e.shiftKey) {return;}e.preventDefault();this.feedback.focus();}, this.grade, 'press:9', this));
-    } else {
-        this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this));                      // Handle Tab and Shift+Tab
+            if (this.grade) {
+                // Override the default tab movements when moving between cells
+                // Handle Shift+Tab.
+                this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9+shift', this));
+
+                // Override the default tab movements for fields in the same cell
+                this.keyevents.push(this.report.Y.on('key',
+                        function(e){e.preventDefault();this.grade.focus();},
+                        this.feedback,
+                        'press:9+shift',
+                        this));
+                this.keyevents.push(this.report.Y.on('key',
+                        function(e){if (e.shiftKey) {return;}e.preventDefault();this.feedback.focus();},
+                        this.grade,
+                        'press:9',
+                        this));
+            }
+        }
+    } else if (this.grade) {
+        // Handle Tab and Shift+Tab.
+        this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this));
+    }
+    if (this.grade) {
+        // Handle the Enter key being pressed.
+        this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'press:13', this));
+        // Handle CTRL + arrow keys.
+        this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'press:37,38,39,40+ctrl', this));
     }
-    this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'press:13', this));                   // Handle the Enter key being pressed
-    this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'press:37,38,39,40+ctrl', this));    // Handle CTRL + arrow keys
 };
 /**
  * Attach the required properties and methods to the existing field class
@@ -741,10 +804,17 @@ M.gradereport_grader.classes.existingfield.prototype.move_focus = function(node)
  * @return {Bool}
  */
 M.gradereport_grader.classes.existingfield.prototype.has_changed = function() {
-    if (this.editfeedback) {
-        return (this.grade.get('value') !== this.oldgrade || this.feedback.get('value') !== this.oldfeedback);
+    if (this.grade) {
+        if (this.grade.get('value') !== this.oldgrade) {
+            return true;
+        }
+    }
+    if (this.editfeedback && this.feedback) {
+        if (this.feedback.get('value') !== this.oldfeedback) {
+            return true;
+        }
     }
-    return (this.grade.get('value') !== this.oldgrade);
+    return false;
 };
 /**
  * Submits any changes and then updates the fields accordingly
@@ -759,15 +829,19 @@ M.gradereport_grader.classes.existingfield.prototype.submit = function() {
 
     var properties = this.report.get_cell_info([this.userid,this.itemid]);
     var values = (function(f){
-        var feedback, oldfeedback = null;
-        if (f.editfeedback) {
+        var feedback, oldfeedback, grade, oldgrade = null;
+        if (f.editfeedback && f.feedback) {
             feedback = f.feedback.get('value');
             oldfeedback = f.oldfeedback;
         }
+        if (f.grade) {
+            grade = f.grade.get('value');
+            oldgrade = f.oldgrade;
+        }
         return {
             editablefeedback : f.editfeedback,
-            grade : f.grade.get('value'),
-            oldgrade : f.oldgrade,
+            grade : grade,
+            oldgrade : oldgrade,
             feedback : feedback,
             oldfeedback : oldfeedback
         };
@@ -799,11 +873,11 @@ M.gradereport_grader.classes.textfield = function(report, node) {
     this.gradespan = node.one('.gradevalue');
     this.inputdiv = this.report.Y.Node.create('<div></div>');
     this.editfeedback = this.report.ajax.showquickfeedback;
-    this.grade = this.report.Y.Node.create('<input type="text" class="text" value="" />');
+    this.grade = this.report.Y.Node.create('<input type="text" class="text" value="" name="ajaxgrade" />');
     this.gradetype = 'value';
     this.inputdiv.append(this.grade);
     if (this.report.ajax.showquickfeedback) {
-        this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" />');
+        this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback" />');
         this.inputdiv.append(this.feedback);
     }
 };
@@ -832,7 +906,11 @@ M.gradereport_grader.classes.textfield.prototype.replace = function() {
         this.set_feedback(this.get_feedback());
     }
     this.node.replaceChild(this.inputdiv, this.gradespan);
-    this.grade.focus();
+    if (this.grade) {
+        this.grade.focus();
+    } else if (this.feedback) {
+        this.feedback.focus();
+    }
     this.editable = true;
     return this;
 };
@@ -845,6 +923,7 @@ M.gradereport_grader.classes.textfield.prototype.replace = function() {
 M.gradereport_grader.classes.textfield.prototype.commit = function() {
     // Produce an anonymous result object contianing all values
     var result = (function(field){
+        // Editable false lets us get the pre-update values.
         field.editable = false;
         var oldgrade = field.get_grade();
         if (oldgrade == '-') {
@@ -855,6 +934,8 @@ M.gradereport_grader.classes.textfield.prototype.commit = function() {
         if (field.editfeedback) {
             oldfeedback = field.get_feedback();
         }
+
+        // Now back to editable gives us the values in the edit areas.
         field.editable = true;
         if (field.editfeedback) {
             feedback = field.get_feedback();
@@ -931,7 +1012,11 @@ M.gradereport_grader.classes.textfield.prototype.set_grade = function(value) {
  */
 M.gradereport_grader.classes.textfield.prototype.get_feedback = function() {
     if (this.editable) {
-        return this.feedback.get('value');
+        if (this.feedback) {
+            return this.feedback.get('value');
+        } else {
+            return null;
+        }
     }
     var properties = this.report.get_cell_info(this.node);
     if (properties) {
@@ -947,7 +1032,9 @@ M.gradereport_grader.classes.textfield.prototype.get_feedback = function() {
  */
 M.gradereport_grader.classes.textfield.prototype.set_feedback = function(value) {
     if (!this.editable) {
-        this.feedback.set('value', value);
+        if (this.feedback) {
+            this.feedback.set('value', value);
+        }
     } else {
         var properties = this.report.get_cell_info(this.node);
         this.report.update_feedback(properties.userid, properties.itemid, value);
@@ -971,7 +1058,12 @@ M.gradereport_grader.classes.textfield.prototype.has_changed = function() {
             return true;
         }
     }
-    return (this.get_grade() != this.gradespan.get('innerHTML'));
+
+    if (this.grade) {
+        return (this.get_grade() != this.gradespan.get('innerHTML'));
+    } else {
+        return false;
+    }
 };
 /**
  * Attaches the key listeners for the editable fields and stored the event references
@@ -984,22 +1076,93 @@ M.gradereport_grader.classes.textfield.prototype.attach_key_events = function()
     var a = this.report.ajax;
     // Setup the default key events for tab and enter
     if (this.editfeedback) {
-        this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9+shift', a));               // Handle Shift+Tab
-        this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'press:9', a, true));            // Handle Tab
-        this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'press:13', a));               // Handle the Enter key being pressed
+        if (this.grade) {
+            // Handle Shift+Tab.
+            this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9+shift', a));
+        }
+        // Handle Tab.
+        this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'press:9', a, true));
+        // Handle the Enter key being pressed.
+        this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'press:13', a));
     } else {
-        this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9', a));                     // Handle Tab and Shift+Tab
-    }
-    this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'press:13', a));                      // Handle the Enter key being pressed
-    // Setup the arrow key events
-    this.keyevents.push(this.report.Y.on('key', a.keypress_arrows, this.grade.ancestor('td'), 'down:37,38,39,40+ctrl', a));       // Handle CTRL + arrow keys
-    // Prevent the default key action on all fields for arrow keys on all key events!
-    // Note: this still does not work in FF!!!!!
-    this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'down:37,38,39,40+ctrl'));
-    this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'press:37,38,39,40+ctrl'));
-    this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'up:37,38,39,40+ctrl'));
+        if (this.grade) {
+            // Handle Tab and Shift+Tab.
+            this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9', a));
+        }
+    }
+
+    // Setup the arrow key events.
+    // Handle CTRL + arrow keys.
+    this.keyevents.push(this.report.Y.on('key', a.keypress_arrows, this.inputdiv.ancestor('td'), 'down:37,38,39,40+ctrl', a));
+
+    if (this.grade) {
+        // Handle the Enter key being pressed.
+        this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'press:13', a));
+        // Prevent the default key action on all fields for arrow keys on all key events!
+        // Note: this still does not work in FF!!!!!
+        this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'down:37,38,39,40+ctrl'));
+        this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'press:37,38,39,40+ctrl'));
+        this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'up:37,38,39,40+ctrl'));
+    }
+};
+
+/**
+ * Feedback field class
+ * This classes gets used in conjunction with the report running with AJAX enabled
+ * and is used to manage a cell that no editable grade, only possibly feedback
+ *
+ * @class feedbackfield
+ * @constructor
+ * @this {M.gradereport_grader.classes.feedbackfield}
+ * @param {M.gradereport_grader.classes.report} report
+ * @param {Y.Node} node
+ */
+M.gradereport_grader.classes.feedbackfield = function(report, node) {
+    this.report = report;
+    this.node = node;
+    this.gradespan = node.one('.gradevalue');
+    this.inputdiv = this.report.Y.Node.create('<div></div>');
+    this.editfeedback = this.report.ajax.showquickfeedback;
+    this.gradetype = 'text';
+    if (this.report.ajax.showquickfeedback) {
+        this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback" />');
+        this.inputdiv.append(this.feedback);
+    }
+};
+
+/**
+ * Gets the grade for current cell (which will always be null)
+ *
+ * @function
+ * @this {M.gradereport_grader.classes.feedbackfield}
+ * @return {Mixed}
+ */
+M.gradereport_grader.classes.feedbackfield.prototype.get_grade = function() {
+    return null;
+};
+
+/**
+ * Overrides the set_grade function of textfield so that it can ignore the set-grade
+ * for grade cells without grades
+ *
+ * @function
+ * @this {M.gradereport_grader.classes.feedbackfield}
+ * @param {String} value
+ */
+M.gradereport_grader.classes.feedbackfield.prototype.set_grade = function() {
+    return;
 };
 
+/**
+ * Manually extend the feedbackfield class with the properties and methods of the
+ * textfield class that have not been defined
+ */
+for (var i in M.gradereport_grader.classes.textfield.prototype) {
+    if (!M.gradereport_grader.classes.feedbackfield.prototype[i]) {
+        M.gradereport_grader.classes.feedbackfield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i];
+    }
+}
+
 /**
  * An editable scale field
  *
@@ -1017,11 +1180,12 @@ M.gradereport_grader.classes.scalefield = function(report, node) {
     this.gradespan = node.one('.gradevalue');
     this.inputdiv = this.report.Y.Node.create('<div></div>');
     this.editfeedback = this.report.ajax.showquickfeedback;
-    this.grade = this.report.Y.Node.create('<select type="text" class="text" /><option value="-1">'+M.util.get_string('ajaxchoosescale', 'gradereport_grader')+'</option></select>');
+    this.grade = this.report.Y.Node.create('<select type="text" class="text" name="ajaxgrade" /><option value="-1">'+
+            M.util.get_string('ajaxchoosescale', 'gradereport_grader')+'</option></select>');
     this.gradetype = 'scale';
     this.inputdiv.append(this.grade);
     if (this.editfeedback) {
-        this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" />');
+        this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback"/>');
         this.inputdiv.append(this.feedback);
     }
     var properties = this.report.get_cell_info(node);
diff --git a/grade/report/grader/tests/behat/ajax_grader.feature b/grade/report/grader/tests/behat/ajax_grader.feature
new file mode 100644 (file)
index 0000000..880a341
--- /dev/null
@@ -0,0 +1,212 @@
+@gradereport @gradereport_grader
+Feature: Using the AJAX grading feature of Grader report to update grades and feedback
+  In order to use AJAX grading
+  As a teacher
+  I need to be able to update and verify grades
+
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1 | 0 | 1 |
+    And the following "users" exist:
+      | username | firstname | lastname | email | idnumber |
+      | teacher1 | Teacher | 1 | teacher1@example.com | t1 |
+      | student1 | Student | 1 | student1@example.com | s1 |
+      | student2 | Student | 2 | student2@example.com | s2 |
+      | student3 | Student | 3 | student3@example.com | s3 |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+      | student2 | C1 | student |
+      | student3 | C1 | student |
+    And the following "scales" exist:
+      | name       | scale                                  |
+      | Test Scale | Disappointing,Good,Very good,Excellent |
+    And the following "grade categories" exist:
+      | fullname | course |
+      | Grade Cat | C1 |
+    And the following "grade items" exist:
+      | itemname | course | locked | gradetype | gradecategory |
+      | Item 1 | C1 | 0 | value | Grade Cat |
+      | Item VU | C1 | 0 | value | Grade Cat |
+      | Item VL | C1 | 1 | value | Grade Cat |
+      | Item TU | C1 | 0 | text | Grade Cat |
+      | Item TL | C1 | 1 | text | Grade Cat |
+    And the following "grade items" exist:
+      | itemname | course | locked | gradetype | scale | gradecategory |
+      | Item SU | C1 | 0 | scale | Test Scale | Grade Cat |
+      | Item SL | C1 | 1 | scale | Test Scale | Grade Cat |
+    And the following "grade items" exist:
+      | itemname | course | locked | gradetype | gradecategory |
+      | Item 3 | C1 | 0 | value | Grade Cat |
+    And the following config values are set as admin:
+      | grade_report_showaverages | 0 |
+      | grade_report_enableajax | 1 |
+
+
+  @javascript
+  Scenario: Use the grader report without editing, with AJAX on and quick feedback off
+    When the following config values are set as admin:
+      | grade_overridecat | 1 |
+      | grade_report_showquickfeedback | 0 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I click on student "Student 2" for grade item "Item VU"
+    Then I should see a grade field for "Student 2" and grade item "Item VU"
+    And I should not see a feedback field for "Student 2" and grade item "Item VU"
+    And I set the field "ajaxgrade" to "33"
+    And I press key "13" in the field "ajaxgrade"
+    And I should not see a grade field for "Student 2" and grade item "Item VU"
+    And I should not see a feedback field for "Student 2" and grade item "Item VU"
+    And I click on student "Student 3" for grade item "Item VU"
+    And I set the field "ajaxgrade" to "50"
+    And I press key "13" in the field "ajaxgrade"
+    And I click on student "Student 3" for grade item "Item 1"
+    And I set the field "ajaxgrade" to "80"
+    And I press key "13" in the field "ajaxgrade"
+    And I click on student "Student 3" for grade item "Item SU"
+    And I set the field "ajaxgrade" to "Very good"
+    And I press key "13" in the field "ajaxgrade"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-      | -5-      | -9-       | -13-         |
+      | Student 2          | -        | 33.00    | -         | 33.00        |
+      | Student 3          | 80.00    | 50.00    | Very good | 133.00       |
+    And I click on student "Student 3" for grade item "Item VL"
+    And I should not see a grade field for "Student 3" and grade item "Item VL"
+    And I should not see a feedback field for "Student 3" and grade item "Item VL"
+    And I click on student "Student 3" for grade item "Item SL"
+    And I should not see a grade field for "Student 3" and grade item "Item SL"
+    And I should not see a feedback field for "Student 3" and grade item "Item SL"
+    And I click on student "Student 3" for grade item "Item TU"
+    And I should not see a grade field for "Student 3" and grade item "Item TU"
+    And I should not see a feedback field for "Student 3" and grade item "Item TU"
+    And I click on student "Student 1" for grade item "Course total"
+    And I should see a grade field for "Student 1" and grade item "Course total"
+    And I should not see a feedback field for "Student 1" and grade item "Course total"
+    And I set the field "ajaxgrade" to "90"
+    And I press key "13" in the field "ajaxgrade"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -13-      |
+      | Student 1          | 90.00     |
+    And I navigate to "Grader report" node in "Grade administration"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-      | -5-      | -9-       | -13-         |
+      | Student 1          | -        | -        | -         | 90.00        |
+      | Student 2          | -        | 33.00    | -         | 33.00        |
+      | Student 3          | 80.00    | 50.00    | Very good | 133.00       |
+
+  @javascript
+  Scenario: Use the grader report without editing, with AJAX and quick feedback on
+    When the following config values are set as admin:
+      | grade_overridecat | 1 |
+      | grade_report_showquickfeedback | 1 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I click on student "Student 2" for grade item "Item VU"
+    Then I should see a grade field for "Student 2" and grade item "Item VU"
+    And I should see a feedback field for "Student 2" and grade item "Item VU"
+    And I set the field "ajaxgrade" to "33"
+    And I set the field "ajaxfeedback" to "Student 2 VU feedback"
+    And I press key "13" in the field "ajaxfeedback"
+    And I click on student "Student 3" for grade item "Item VL"
+    And I should not see a grade field for "Student 3" and grade item "Item VL"
+    And I should not see a feedback field for "Student 3" and grade item "Item VL"
+    And I click on student "Student 3" for grade item "Item TU"
+    And I should not see a grade field for "Student 3" and grade item "Item TU"
+    And I should see a feedback field for "Student 3" and grade item "Item TU"
+    And I set the field "ajaxfeedback" to "Student 3 TU feedback"
+    And I press key "13" in the field "ajaxfeedback"
+    And I click on student "Student 2" for grade item "Item SU"
+    And I set the field "ajaxgrade" to "Very good"
+    And I set the field "ajaxfeedback" to "Student 2 SU feedback"
+    And I press key "13" in the field "ajaxfeedback"
+    And I navigate to "Grader report" node in "Grade administration"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -5-      | -9-       | -13-         |
+      | Student 2          | 33.00    | Very good | 36.00        |
+    And I click on student "Student 3" for grade item "Item TU"
+    And the field "ajaxfeedback" matches value "Student 3 TU feedback"
+    And I click on student "Student 2" for grade item "Item SU"
+    And the field "ajaxfeedback" matches value "Student 2 SU feedback"
+
+  @javascript
+  Scenario: Use the grader report without editing, with AJAX and quick feedback on, without category override
+    When the following config values are set as admin:
+      | grade_overridecat | 0 |
+      | grade_report_showquickfeedback | 1 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I click on student "Student 2" for grade item "Item VU"
+    Then I should see a grade field for "Student 2" and grade item "Item VU"
+    And I should see a feedback field for "Student 2" and grade item "Item VU"
+    And I set the field "ajaxgrade" to "33"
+    And I press key "13" in the field "ajaxgrade"
+    And I click on student "Student 2" for grade item "Course total"
+    And I should not see a grade field for "Student 3" and grade item "Course total"
+    And I should not see a feedback field for "Student 3" and grade item "Course total"
+    And the following should exist in the "user-grades" table:
+      | -1-                | -5-      | -13-    |
+      | Student 2          | 33.00    | 33.00   |
+
+  @javascript
+  Scenario: Use the grader report with editing, with AJAX and quick feedback on, with category override
+    When the following config values are set as admin:
+      | grade_overridecat | 1 |
+      | grade_report_showquickfeedback | 1 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    Then I should not see a grade field for "Student 2" and grade item "Item VL"
+    And I should not see a feedback field for "Student 2" and grade item "Item VL"
+    And I should not see a grade field for "Student 2" and grade item "Item TU"
+    And I should see a feedback field for "Student 2" and grade item "Item TU"
+    And I should see a grade field for "Student 2" and grade item "Course total"
+    And I should see a feedback field for "Student 2" and grade item "Course total"
+    And I give the grade "20.00" to the user "Student 2" for the grade item "Item VU"
+    And I click away from student "Student 2" and grade item "Item VU" value
+    And I give the grade "30.00" to the user "Student 2" for the grade item "Item 1"
+    And I give the feedback "Some feedback" to the user "Student 2" for the grade item "Item 1"
+    And I click away from student "Student 2" and grade item "Item 1" feedback
+    And I give the grade "Very good" to the user "Student 2" for the grade item "Item SU"
+    And I click away from student "Student 2" and grade item "Item SU" value
+    And the grade for "Student 2" in grade item "Grade Cat" should match "53.00"
+    And the grade for "Student 2" in grade item "Course total" should match "53.00"
+    And I turn editing mode off
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-      | -5-     | -9-       | -12-     | -13-    |
+      | Student 2          | 30.00    | 20.00   | Very good | 53.00    | 53.00   |
+    And I click on student "Student 2" for grade item "Item 1"
+    And the field "ajaxfeedback" matches value "Some feedback"
+
+  @javascript
+  Scenario: Use the grader report with editing, with AJAX and quick feedback on, without category override
+    When the following config values are set as admin:
+      | grade_overridecat | 0 |
+      | grade_report_showquickfeedback | 1 |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I navigate to "Grades" node in "Course administration"
+    And I turn editing mode on
+    Then I should not see a grade field for "Student 2" and grade item "Course total"
+    And I should not see a feedback field for "Student 2" and grade item "Course total"
+    And I give the grade "20.00" to the user "Student 2" for the grade item "Item VU"
+    And I click away from student "Student 2" and grade item "Item VU" value
+    And I give the grade "30.00" to the user "Student 2" for the grade item "Item 1"
+    And I click away from student "Student 2" and grade item "Item 1" value
+    And I give the feedback "Some feedback" to the user "Student 2" for the grade item "Item 1"
+    And I click away from student "Student 2" and grade item "Item 1" feedback
+    And the following should exist in the "user-grades" table:
+      | -1-                | -13-     |
+      | Student 2          | 50.00    |
+    And I turn editing mode off
+    And the following should exist in the "user-grades" table:
+      | -1-                | -4-      | -5-      | -13-         |
+      | Student 2          | 30.00    | 20.00    | 50.00        |
+    And I click on student "Student 2" for grade item "Item 1"
+    And the field "ajaxfeedback" matches value "Some feedback"
diff --git a/grade/report/grader/tests/behat/behat_gradereport_grader.php b/grade/report/grader/tests/behat/behat_gradereport_grader.php
new file mode 100644 (file)
index 0000000..03e93ee
--- /dev/null
@@ -0,0 +1,273 @@
+<?php
+// This file is part of Stack - http://stack.bham.ac.uk/
+//
+// Stack is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Stack is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Stack.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Behat steps definitions for drag and drop onto image.
+ *
+ * @package   gradereport_grader
+ * @category  test
+ * @copyright 2015 Oakland University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
+
+require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php');
+
+use Behat\Behat\Context\Step\Given,
+    Behat\Behat\Context\Step\Then,
+    Behat\Mink\Exception\ExpectationException as ExpectationException,
+    Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+
+/**
+ * Steps definitions related with the drag and drop onto image question type.
+ *
+ * @copyright 2015 Oakland University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class behat_gradereport_grader extends behat_base {
+    /**
+     * Click a given user grade cell.
+     *
+     * @Given /^I click on student "([^"]*)" for grade item "([^"]*)"$/
+     * @param string $student
+     * @param string $itemname
+     * @return Given
+     */
+    public function i_click_on_student_and_grade_item($student, $itemname) {
+        $xpath = $this->get_student_and_grade_cell_selector($student, $itemname);
+
+        return new Given('I click on "' . $this->escape($xpath) . '" "xpath_element"');
+    }
+
+    /**
+     * Remove focus for a grade value cell.
+     *
+     * @Given /^I click away from student "([^"]*)" and grade item "([^"]*)" value$/
+     * @param string $student
+     * @param string $itemname
+     * @return Given
+     */
+    public function i_click_away_from_student_and_grade_value($student, $itemname) {
+        $xpath = $this->get_student_and_grade_value_selector($student, $itemname);
+
+        return new Given('I take focus off "' . $this->escape($xpath) . '" "xpath_element"');
+    }
+
+    /**
+     * Remove focus for a grade value cell.
+     *
+     * @Given /^I click away from student "([^"]*)" and grade item "([^"]*)" feedback$/
+     * @param string $student
+     * @param string $itemname
+     * @return Given
+     */
+    public function i_click_away_from_student_and_grade_feedback($student, $itemname) {
+        $xpath = $this->get_student_and_grade_feedback_selector($student, $itemname);
+
+        return new Given('I take focus off "' . $this->escape($xpath) . '" "xpath_element"');
+    }
+
+    /**
+     * Checks grade values with or without a edit box.
+     *
+     * @Then /^the grade for "([^"]*)" in grade item "([^"]*)" should match "([^"]*)"$/
+     * @throws Exception
+     * @throws ElementNotFoundException
+     * @param string $student
+     * @param string $itemname
+     * @param string $value
+     * @return Then
+     */
+    public function the_grade_should_match($student, $itemname, $value) {
+        $xpath = $this->get_student_and_grade_value_selector($student, $itemname);
+
+        $gradefield = $this->getSession()->getPage()->find('xpath', $xpath);
+        if (!empty($gradefield)) {
+            // Get the field.
+            $fieldtype = behat_field_manager::guess_field_type($gradefield, $this->getSession());
+            if (!$fieldtype) {
+                throw new Exception('Could not get field type for grade field "' . $itemname . '"');
+            }
+            $field = behat_field_manager::get_field_instance($fieldtype, $gradefield, $this->getSession());
+            if (!$field->matches($value)) {
+                $fieldvalue = $field->get_value();
+                throw new ExpectationException(
+                    'The "' . $student . '" and "' . $itemname . '" grade is "' . $fieldvalue . '", "' . $value . '" expected' ,
+                    $this->getSession()
+                );
+            }
+        } else {
+            // If there isn't a form field, just search for contents.
+            $valueliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($value);
+
+            $xpath = $this->get_student_and_grade_cell_selector($student, $itemname);
+            $xpath .= "[contains(normalize-space(.)," . $valueliteral . ")]";
+
+            $node = $this->getSession()->getDriver()->find($xpath);
+            if (empty($node)) {
+                $locatorexceptionmsg = 'Cell for "' . $student . '" and "' . $itemname . '" with value "' . $value . '"';
+                throw new ElementNotFoundException($this->getSession(), $locatorexceptionmsg, null, $xpath);
+            }
+        }
+    }
+
+    /**
+     * Look for a grade editing field.
+     *
+     * @Then /^I should see a grade field for "([^"]*)" and grade item "([^"]*)"$/
+     * @param string $student
+     * @param string $itemname
+     * @return Then
+     */
+    public function i_should_see_grade_field($student, $itemname) {
+        $xpath = $this->get_student_and_grade_value_selector($student, $itemname);
+
+        return new Then('"' . $this->escape($xpath) . '" "xpath_element" should be visible');
+    }
+
+    /**
+     * Look for a feedback editing field.
+     *
+     * @Then /^I should see a feedback field for "([^"]*)" and grade item "([^"]*)"$/
+     * @param string $student
+     * @param string $itemname
+     * @return Then
+     */
+    public function i_should_see_feedback_field($student, $itemname) {
+        $xpath = $this->get_student_and_grade_feedback_selector($student, $itemname);
+
+        return new Then('"' . $this->escape($xpath) . '" "xpath_element" should be visible');
+    }
+
+    /**
+     * Look for a lack of the grade editing field.
+     *
+     * @Then /^I should not see a grade field for "([^"]*)" and grade item "([^"]*)"$/
+     * @param string $student
+     * @param string $itemname
+     * @return Then
+     */
+    public function i_should_not_see_grade_field($student, $itemname) {
+        $xpath = $this->get_student_and_grade_value_selector($student, $itemname);
+
+        return new Then('"' . $this->escape($xpath) . '" "xpath_element" should not exist');
+    }
+
+    /**
+     * Look for a lack of the feedback editing field.
+     *
+     * @Then /^I should not see a feedback field for "([^"]*)" and grade item "([^"]*)"$/
+     * @param string $student
+     * @param string $itemname
+     * @return Then
+     */
+    public function i_should_not_see_feedback_field($student, $itemname) {
+        $xpath = $this->get_student_and_grade_feedback_selector($student, $itemname);
+
+        return new Then('"' . $this->escape($xpath) . '" "xpath_element" should not exist');
+    }
+
+    /**
+     * Gets the user id from its name.
+     *
+     * @throws Exception
+     * @param string $name
+     * @return int
+     */
+    protected function get_user_id($name) {
+        global $DB;
+        $names = explode(' ', $name);
+
+        if (!$id = $DB->get_field('user', 'id', array('firstname' => $names[0], 'lastname' => $names[1]))) {
+            throw new Exception('The specified user with username "' . $name . '" does not exist');
+        }
+        return $id;
+    }
+
+    /**
+     * Gets the grade item id from its name.
+     *
+     * @throws Exception
+     * @param string $itemname
+     * @return int
+     */
+    protected function get_grade_item_id($itemname) {
+        global $DB;
+
+        if ($id = $DB->get_field('grade_items', 'id', array('itemname' => $itemname))) {
+            return $id;
+        }
+
+        // The course total is a special case.
+        if ($itemname === "Course total") {
+            if (!$id = $DB->get_field('grade_items', 'id', array('itemtype' => 'course'))) {
+                throw new Exception('The specified grade_item with name "' . $itemname . '" does not exist');
+            }
+            return $id;
+        }
+
+        // Find a category with the name.
+        if ($catid = $DB->get_field('grade_categories', 'id', array('fullname' => $itemname))) {
+            if ($id = $DB->get_field('grade_items', 'id', array('iteminstance' => $catid))) {
+                return $id;
+            }
+        }
+
+        throw new Exception('The specified grade_item with name "' . $itemname . '" does not exist');
+    }
+
+    /**
+     * Gets unique xpath selector for a student/grade item combo.
+     *
+     * @throws Exception
+     * @param string $student
+     * @param string $itemname
+     * @return string
+     */
+    protected function get_student_and_grade_cell_selector($student, $itemname) {
+        $itemid = 'u' . $this->get_user_id($student) . 'i' . $this->get_grade_item_id($itemname);
+        return "//table[@id='user-grades']//td[@id='" . $itemid . "']";
+    }
+
+    /**
+     * Gets xpath for a particular student/grade item grade value cell.
+     *
+     * @throws Exception
+     * @param string $student
+     * @param string $itemname
+     * @return string
+     */
+    protected function get_student_and_grade_value_selector($student, $itemname) {
+        $cell = $this->get_student_and_grade_cell_selector($student, $itemname);
+        return $cell . "//*[contains(@id, 'grade_') or @name='ajaxgrade']";
+    }
+
+    /**
+     * Gets xpath for a particular student/grade item feedback cell.
+     *
+     * @throws Exception
+     * @param string $student
+     * @param string $itemname
+     * @return string
+     */
+    protected function get_student_and_grade_feedback_selector($student, $itemname) {
+        $cell = $this->get_student_and_grade_cell_selector($student, $itemname);
+        return $cell . "//input[contains(@id, 'feedback_') or @name='ajaxfeedback']";
+    }
+
+
+}
index 727aeb0..6ee26bf 100644 (file)
@@ -48,6 +48,23 @@ class behat_grade extends behat_base {
         return new Given('I set the field "' . $this->escape($fieldstr) . '" to "' . $grade . '"');
     }
 
+    /**
+     * Enters a quick feedback via the gradebook for a specific grade item and user when viewing
+     * the 'Grader report' with editing mode turned on.
+     *
+     * @Given /^I give the feedback "(?P<grade_number>(?:[^"]|\\")*)" to the user "(?P<username_string>(?:[^"]|\\")*)" for the grade item "(?P<grade_activity_string>(?:[^"]|\\")*)"$/
+     * @param string $feedback
+     * @param string $userfullname the user's fullname as returned by fullname()
+     * @param string $itemname
+     * @return Given
+     */
+    public function i_give_the_feedback($feedback, $userfullname, $itemname) {
+        $gradelabel = $userfullname . ' ' . $itemname;
+        $fieldstr = get_string('useractivityfeedback', 'gradereport_grader', $gradelabel);
+
+        return new Given('I set the field "' . $this->escape($fieldstr) . '" to "' . $this->escape($feedback) . '"');
+    }
+
     /**
      * Changes the settings of a grade item or category or the course.
      *