From 12f946d99904cf2dda79306b5d2d7380712e18ba Mon Sep 17 00:00:00 2001 From: Eric Merrill Date: Tue, 6 Oct 2015 11:30:00 -0400 Subject: [PATCH] MDL-36606 gradereport: Update AJAX grader to understand gradeless cells --- grade/report/grader/lib.php | 27 +- grade/report/grader/module.js | 290 +++++++++++++----- .../grader/tests/behat/ajax_grader.feature | 212 +++++++++++++ .../tests/behat/behat_gradereport_grader.php | 273 +++++++++++++++++ grade/tests/behat/behat_grade.php | 17 + 5 files changed, 741 insertions(+), 78 deletions(-) create mode 100644 grade/report/grader/tests/behat/ajax_grader.feature create mode 100644 grade/report/grader/tests/behat/behat_gradereport_grader.php diff --git a/grade/report/grader/lib.php b/grade/report/grader/lib.php index ff75d902a88..85d3a9536a5 100644 --- a/grade/report/grader/lib.php +++ b/grade/report/grader/lib.php @@ -747,7 +747,7 @@ class grade_report_grader extends grade_report { 'items' => array(), 'users' => array(), 'feedback' => array(), - 'grades' => array(), + 'grades' => array() ); $jsscales = array(); @@ -1001,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 @@ -1070,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'; } } @@ -1114,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; diff --git a/grade/report/grader/module.js b/grade/report/grader/module.js index 7bf49d2be81..2fac542b5bd 100644 --- a/grade/report/grader/module.js +++ b/grade/report/grader/module.js @@ -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,9 +633,10 @@ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) { this.editfeedback = ajax.showquickfeedback; this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid); - if (this.grade !== null) { - 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']; } } @@ -609,7 +649,6 @@ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) { // On blur save any changes in the grade field this.grade.on('blur', this.submit, this); - } // Check if feedback is enabled @@ -617,9 +656,9 @@ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) { // Get the feedback fields this.feedback = this.report.Y.one('#feedback_'+userid+'_'+itemid); - if (this.feedback !== null) { - 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) { + 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']; } } @@ -634,28 +673,40 @@ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) { 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.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 + // 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)); + 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)); - if (this.grade !== null) { // 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)); - - // Override the default tab movements when moving between cells - if (this.editfeedback) { - 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', + 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 !== null) { - this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this)); // Handle Tab and Shift+Tab + } 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.feedback !== null) { - 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 + 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)); } }; /** @@ -753,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; + } } - return (this.grade.get('value') !== this.oldgrade); + if (this.editfeedback && this.feedback) { + if (this.feedback.get('value') !== this.oldfeedback) { + return true; + } + } + return false; }; /** * Submits any changes and then updates the fields accordingly @@ -771,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 }; @@ -811,11 +873,11 @@ M.gradereport_grader.classes.textfield = function(report, node) { this.gradespan = node.one('.gradevalue'); this.inputdiv = this.report.Y.Node.create('
'); this.editfeedback = this.report.ajax.showquickfeedback; - this.grade = this.report.Y.Node.create(''); + this.grade = this.report.Y.Node.create(''); this.gradetype = 'value'; this.inputdiv.append(this.grade); if (this.report.ajax.showquickfeedback) { - this.feedback = this.report.Y.Node.create(''); + this.feedback = this.report.Y.Node.create(''); this.inputdiv.append(this.feedback); } }; @@ -844,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; }; @@ -857,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 == '-') { @@ -867,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(); @@ -943,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) { @@ -959,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); @@ -983,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 @@ -996,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('
'); + this.editfeedback = this.report.ajax.showquickfeedback; + this.gradetype = 'text'; + if (this.report.ajax.showquickfeedback) { + this.feedback = this.report.Y.Node.create(''); + 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 * @@ -1029,11 +1180,12 @@ M.gradereport_grader.classes.scalefield = function(report, node) { this.gradespan = node.one('.gradevalue'); this.inputdiv = this.report.Y.Node.create('
'); this.editfeedback = this.report.ajax.showquickfeedback; - this.grade = this.report.Y.Node.create(''); + this.grade = this.report.Y.Node.create(''); this.gradetype = 'scale'; this.inputdiv.append(this.grade); if (this.editfeedback) { - this.feedback = this.report.Y.Node.create(''); + this.feedback = this.report.Y.Node.create(''); 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 index 00000000000..880a34144b1 --- /dev/null +++ b/grade/report/grader/tests/behat/ajax_grader.feature @@ -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 index 00000000000..03e93ee7d39 --- /dev/null +++ b/grade/report/grader/tests/behat/behat_gradereport_grader.php @@ -0,0 +1,273 @@ +. + +/** + * 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']"; + } + + +} diff --git a/grade/tests/behat/behat_grade.php b/grade/tests/behat/behat_grade.php index 727aeb0e71e..6ee26bf2501 100644 --- a/grade/tests/behat/behat_grade.php +++ b/grade/tests/behat/behat_grade.php @@ -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(?:[^"]|\\")*)" to the user "(?P(?:[^"]|\\")*)" for the grade item "(?P(?:[^"]|\\")*)"$/ + * @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. * -- 2.43.0