MDL-36606 gradereport: Update AJAX grader to understand gradeless cells

This commit is contained in:
Eric Merrill 2015-10-06 11:30:00 -04:00
parent df65120a75
commit 12f946d999
5 changed files with 741 additions and 78 deletions

View File

@ -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;

View 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,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
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));
// 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
if (this.editfeedback) {
this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9+shift', this)); // Handle Shift+Tab
}
// 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 !== 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('<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);
}
};
@ -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
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'));
}
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'));
};
/**
* 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
*
@ -1029,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);

View File

@ -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"

View File

@ -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']";
}
}

View 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.
*