Merge branch 'MDL-36606-master' of git://github.com/merrill-oakland/moodle
[moodle.git] / grade / report / grader / module.js
1 /**
2  * Grader report namespace
3  */
4 M.gradereport_grader = {
5     /**
6      * @namespace M.gradereport_grader
7      * @param {Object} reports A collection of classes used by the grader report module
8      */
9     classes : {},
10     /**
11      * Instantiates a new grader report
12      *
13      * @function
14      * @param {YUI} Y
15      * @param {Object} cfg A configuration object
16      * @param {Array} An array of items in the report
17      * @param {Array} An array of users on the report
18      * @param {Array} An array of feedback objects
19      * @param {Array} An array of student grades
20      */
21     init_report : function(Y, cfg, items, users, feedback, grades) {
22         // Create the actual report
23         new this.classes.report(Y, cfg, items, users, feedback, grades);
24     }
25 };
27 /**
28  * Initialises the JavaScript for the gradebook grader report
29  *
30  * The functions fall into 3 groups:
31  * M.gradereport_grader.classes.ajax Used when editing is off and fields are dynamically added and removed
32  * M.gradereport_grader.classes.existingfield Used when editing is on meaning all fields are already displayed
33  * M.gradereport_grader.classes.report Common to both of the above
34  *
35  * @class report
36  * @constructor
37  * @this {M.gradereport_grader}
38  * @param {YUI} Y
39  * @param {Object} cfg Configuration variables
40  * @param {Array} items An array containing grade items
41  * @param {Array} users An array containing user information
42  * @param {Array} feedback An array containing feedback information
43  */
44 M.gradereport_grader.classes.report = function(Y, cfg, items, users, feedback, grades) {
45     this.Y = Y;
46     this.isediting = (cfg.isediting);
47     this.ajaxenabled = (cfg.ajaxenabled);
48     this.items = items;
49     this.users = users;
50     this.feedback = feedback;
51     this.table = Y.one('#user-grades');
52     this.grades = grades;
54     // If ajax is enabled then initialise the ajax component
55     if (this.ajaxenabled) {
56         this.ajax = new M.gradereport_grader.classes.ajax(this, cfg);
57     }
58 };
59 /**
60  * Extend the report class with the following methods and properties
61  */
62 M.gradereport_grader.classes.report.prototype.table = null;           // YUI Node for the reports main table
63 M.gradereport_grader.classes.report.prototype.items = [];             // Array containing grade items
64 M.gradereport_grader.classes.report.prototype.users = [];             // Array containing user information
65 M.gradereport_grader.classes.report.prototype.feedback = [];          // Array containing feedback items
66 M.gradereport_grader.classes.report.prototype.ajaxenabled = false;    // True is AJAX is enabled for the report
67 M.gradereport_grader.classes.report.prototype.ajax = null;            // An instance of the ajax class or null
68 /**
69  * Builds an object containing information at the relevant cell given either
70  * the cell to get information for or an array containing userid and itemid
71  *
72  * @function
73  * @this {M.gradereport_grader}
74  * @param {Y.Node|Array} arg Either a YUI Node instance or an array containing
75  *                           the userid and itemid to reference
76  * @return {Object}
77  */
78 M.gradereport_grader.classes.report.prototype.get_cell_info = function(arg) {
80     var userid= null;
81     var itemid = null;
82     var feedback = ''; // Don't default feedback to null or string comparisons become error prone
83     var cell = null;
84     var i = null;
86     if (arg instanceof this.Y.Node) {
87         if (arg.get('nodeName').toUpperCase() !== 'TD') {
88             arg = arg.ancestor('td.cell');
89         }
90         var regexp = /^u(\d+)i(\d+)$/;
91         var parts = regexp.exec(arg.getAttribute('id'));
92         userid = parts[1];
93         itemid = parts[2];
94         cell = arg;
95     } else {
96         userid = arg[0];
97         itemid = arg[1];
98         cell = this.Y.one('#u'+userid+'i'+itemid);
99     }
101     if (!cell) {
102         return null;
103     }
105     for (i in this.feedback) {
106         if (this.feedback[i] && this.feedback[i].user == userid && this.feedback[i].item == itemid) {
107             feedback = this.feedback[i].content;
108             break;
109         }
110     }
112     return {
113         id : cell.getAttribute('id'),
114         userid : userid,
115         username : this.users[userid],
116         itemid : itemid,
117         itemname : this.items[itemid].name,
118         itemtype : this.items[itemid].type,
119         itemscale : this.items[itemid].scale,
120         itemdp : this.items[itemid].decimals,
121         feedback : feedback,
122         cell : cell
123     };
124 };
125 /**
126  * Updates or creates the feedback JS structure for the given user/item
127  *
128  * @function
129  * @this {M.gradereport_grader}
130  * @param {Int} userid
131  * @param {Int} itemid
132  * @param {String} newfeedback
133  * @return {Bool}
134  */
135 M.gradereport_grader.classes.report.prototype.update_feedback = function(userid, itemid, newfeedback) {
136     for (var i in this.feedback) {
137         if (this.feedback[i].user == userid && this.feedback[i].item == itemid) {
138             this.feedback[i].content = newfeedback;
139             return true;
140         }
141     }
142     this.feedback.push({user:userid,item:itemid,content:newfeedback});
143     return true;
144 };
145 /**
146  * Initialises the AJAX component of this report
147  * @class ajax
148  * @constructor
149  * @this {M.gradereport_grader.ajax}
150  * @param {M.gradereport_grader.classes.report} report
151  * @param {Object} cfg
152  */
153 M.gradereport_grader.classes.ajax = function(report, cfg) {
154     this.report = report;
155     this.courseid = cfg.courseid || null;
156     this.feedbacktrunclength = cfg.feedbacktrunclength || null;
157     this.studentsperpage = cfg.studentsperpage || null;
158     this.showquickfeedback = cfg.showquickfeedback || false;
159     this.scales = cfg.scales || null;
160     this.existingfields = [];
162     if (!report.isediting) {
163         report.table.all('.clickable').on('click', this.make_editable, this);
164     } else {
165         for (var userid in report.users) {
166             if (!this.existingfields[userid]) {
167                 this.existingfields[userid] = [];
168             }
169             for (var itemid in report.items) {
170                 this.existingfields[userid][itemid] = new M.gradereport_grader.classes.existingfield(this, userid, itemid);
171             }
172         }
173         // Disable the Update button as we're saving using ajax.
174         submitbutton = this.report.Y.one('#gradersubmit');
175         submitbutton.set('disabled', true);
176     }
177 };
178 /**
179  * Extend the ajax class with the following methods and properties
180  */
181 M.gradereport_grader.classes.ajax.prototype.report = null;                  // A reference to the report class this object will use
182 M.gradereport_grader.classes.ajax.prototype.courseid = null;                // The id for the course being viewed
183 M.gradereport_grader.classes.ajax.prototype.feedbacktrunclength = null;     // The length to truncate feedback to
184 M.gradereport_grader.classes.ajax.prototype.studentsperpage = null;         // The number of students shown per page
185 M.gradereport_grader.classes.ajax.prototype.showquickfeedback = null;       // True if feedback editing should be shown
186 M.gradereport_grader.classes.ajax.prototype.current = null;                 // The field being currently editing
187 M.gradereport_grader.classes.ajax.prototype.pendingsubmissions = [];        // Array containing pending IO transactions
188 M.gradereport_grader.classes.ajax.prototype.scales = [];                    // An array of scales used in this report
189 /**
190  * Makes a cell editable
191  * @function
192  * @this {M.gradereport_grader.classes.ajax}
193  */
194 M.gradereport_grader.classes.ajax.prototype.make_editable = function(e) {
195     var node = e;
196     if (e.halt) {
197         e.halt();
198         node = e.target;
199     }
200     if (node.get('nodeName').toUpperCase() !== 'TD') {
201         node = node.ancestor('td');
202     }
203     this.report.Y.detach('click', this.make_editable, node);
205     if (this.current) {
206         // Current is already set!
207         this.process_editable_field(node);
208         return;
209     }
211     // Sort out the field type
212     var fieldtype = 'value';
213     if (node.hasClass('grade_type_scale')) {
214         fieldtype = 'scale';
215     } else if (node.hasClass('grade_type_text')) {
216         fieldtype = 'text';
217     }
218     // Create the appropriate field widget
219     switch (fieldtype) {
220         case 'scale':
221             this.current = new M.gradereport_grader.classes.scalefield(this.report, node);
222             break;
223         case 'text':
224             this.current = new M.gradereport_grader.classes.feedbackfield(this.report, node);
225             break;
226         default:
227             this.current = new M.gradereport_grader.classes.textfield(this.report, node);
228             break;
229     }
230     this.current.replace().attach_key_events();
232     // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
233     Y.Global.fire('moodle-gradereport_grader:resized');
234 };
235 /**
236  * Callback function for the user pressing the enter key on an editable field
237  *
238  * @function
239  * @this {M.gradereport_grader.classes.ajax}
240  * @param {Event} e
241  */
242 M.gradereport_grader.classes.ajax.prototype.keypress_enter = function(e) {
243     this.process_editable_field(null);
244 };
245 /**
246  * Callback function for the user pressing Tab or Shift+Tab
247  *
248  * @function
249  * @this {M.gradereport_grader.classes.ajax}
250  * @param {Event} e
251  * @param {Bool} ignoreshift If true and shift is pressed then don't exec
252  */
253 M.gradereport_grader.classes.ajax.prototype.keypress_tab = function(e, ignoreshift) {
254     var next = null;
255     if (e.shiftKey) {
256         if (ignoreshift) {
257             return;
258         }
259         next = this.get_above_cell();
260     } else {
261         next = this.get_below_cell();
262     }
263     this.process_editable_field(next);
264 };
265 /**
266  * Callback function for the user pressing an CTRL + an arrow key
267  *
268  * @function
269  * @this {M.gradereport_grader.classes.ajax}
270  */
271 M.gradereport_grader.classes.ajax.prototype.keypress_arrows = function(e) {
272     e.preventDefault();
273     var next = null;
274     switch (e.keyCode) {
275         case 37:    // Left
276             next = this.get_prev_cell();
277             break;
278         case 38:    // Up
279             next = this.get_above_cell();
280             break;
281         case 39:    // Right
282             next = this.get_next_cell();
283             break;
284         case 40:    // Down
285             next = this.get_below_cell();
286             break;
287     }
288     this.process_editable_field(next);
289 };
290 /**
291  * Processes an editable field an does what ever is required to update it
292  *
293  * @function
294  * @this {M.gradereport_grader.classes.ajax}
295  * @param {Y.Node|null} next The next node to make editable (chaining)
296  */
297 M.gradereport_grader.classes.ajax.prototype.process_editable_field = function(next) {
298     if (this.current.has_changed()) {
299         var properties = this.report.get_cell_info(this.current.node);
300         var values = this.current.commit();
301         this.current.revert();
302         this.submit(properties, values);
303     } else {
304         this.current.revert();
305     }
306     this.current = null;
307     if (next) {
308         this.make_editable(next, null);
309     }
311     // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
312     Y.Global.fire('moodle-gradereport_grader:resized');
313 };
314 /**
315  * Gets the next cell that is editable (right)
316  * @function
317  * @this {M.gradereport_grader.classes.ajax}
318  * @param {Y.Node} cell
319  * @return {Y.Node}
320  */
321 M.gradereport_grader.classes.ajax.prototype.get_next_cell = function(cell) {
322     var n = cell || this.current.node;
323     var next = n.next('td');
324     var tr = null;
325     if (!next && (tr = n.ancestor('tr').next('tr'))) {
326         next = tr.all('.grade').item(0);
327     }
328     if (!next) {
329         return this.current.node;
330     }
331     // Continue on until we find a clickable cell
332     if (!next.hasClass('clickable')) {
333         return this.get_next_cell(next);
334     }
335     return next;
336 };
337 /**
338  * Gets the previous cell that is editable (left)
339  * @function
340  * @this {M.gradereport_grader.classes.ajax}
341  * @param {Y.Node} cell
342  * @return {Y.Node}
343  */
344 M.gradereport_grader.classes.ajax.prototype.get_prev_cell = function(cell) {
345     var n = cell || this.current.node;
346     var next = n.previous('.grade');
347     var tr = null;
348     if (!next && (tr = n.ancestor('tr').previous('tr'))) {
349         var cells = tr.all('.grade');
350         next = cells.item(cells.size()-1);
351     }
352     if (!next) {
353         return this.current.node;
354     }
355     // Continue on until we find a clickable cell
356     if (!next.hasClass('clickable')) {
357         return this.get_prev_cell(next);
358     }
359     return next;
360 };
361 /**
362  * Gets the cell above if it is editable (up)
363  * @function
364  * @this {M.gradereport_grader.classes.ajax}
365  * @param {Y.Node} cell
366  * @return {Y.Node}
367  */
368 M.gradereport_grader.classes.ajax.prototype.get_above_cell = function(cell) {
369     var n = cell || this.current.node;
370     var tr = n.ancestor('tr').previous('tr');
371     var next = null;
372     if (tr) {
373         var column = 0;
374         var ntemp = n;
375         while (ntemp = ntemp.previous('td.cell')) {
376             column++;
377         }
378         next = tr.all('td.cell').item(column);
379     }
380     if (!next) {
381         return this.current.node;
382     }
383     // Continue on until we find a clickable cell
384     if (!next.hasClass('clickable')) {
385         return this.get_above_cell(next);
386     }
387     return next;
388 };
389 /**
390  * Gets the cell below if it is editable (down)
391  * @function
392  * @this {M.gradereport_grader.classes.ajax}
393  * @param {Y.Node} cell
394  * @return {Y.Node}
395  */
396 M.gradereport_grader.classes.ajax.prototype.get_below_cell = function(cell) {
397     var n = cell || this.current.node;
398     var tr = n.ancestor('tr').next('tr');
399     var next = null;
400     if (tr && !tr.hasClass('avg')) {
401         var column = 0;
402         var ntemp = n;
403         while (ntemp = ntemp.previous('td.cell')) {
404             column++;
405         }
406         next = tr.all('td.cell').item(column);
407     }
408     if (!next) {
409         return this.current.node;
410     }
411     // Continue on until we find a clickable cell
412     if (!next.hasClass('clickable')) {
413         return this.get_below_cell(next);
414     }
415     return next;
416 };
417 /**
418  * Submits changes for update
419  *
420  * @function
421  * @this {M.gradereport_grader.classes.ajax}
422  * @param {Object} properties Properties of the cell being edited
423  * @param {Object} values Object containing old + new values
424  */
425 M.gradereport_grader.classes.ajax.prototype.submit = function(properties, values) {
426     // Stop the IO queue so we can add to it
427     this.report.Y.io.queue.stop();
428     // If the grade has changed add an IO transaction to update it to the queue
429     if (values.grade !== values.oldgrade) {
430         this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', {
431             method : 'POST',
432             data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.grade+'&type='+properties.itemtype+'&sesskey='+M.cfg.sesskey,
433             on : {
434                 complete : this.submission_outcome
435             },
436             context : this,
437             arguments : {
438                 properties : properties,
439                 values : values,
440                 type : 'grade'
441             }
442         }),complete:false,outcome:null});
443     }
444     // If feedback is editable and has changed add to the IO queue for it
445     if (values.editablefeedback && values.feedback !== values.oldfeedback) {
446         this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', {
447             method : 'POST',
448             data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.feedback+'&type=feedback&sesskey='+M.cfg.sesskey,
449             on : {
450                 complete : this.submission_outcome
451             },
452             context : this,
453             arguments : {
454                 properties : properties,
455                 values : values,
456                 type : 'feedback'
457             }
458         }),complete:false,outcome:null});
459     }
460     // Process the IO queue
461     this.report.Y.io.queue.start();
462 };
463 /**
464  * Callback function for IO transaction completions
465  *
466  * Uses a synchronous queue to ensure we maintain some sort of order
467  *
468  * @function
469  * @this {M.gradereport_grader.classes.ajax}
470  * @param {Int} tid Transaction ID
471  * @param {Object} outcome
472  * @param {Mixed} args
473  */
474 M.gradereport_grader.classes.ajax.prototype.submission_outcome = function(tid, outcome, args) {
475     // Parse the response as JSON
476     try {
477         outcome = this.report.Y.JSON.parse(outcome.responseText);
478     } catch(e) {
479         var message = M.util.get_string('ajaxfailedupdate', 'gradereport_grader');
480         message = message.replace(/\[1\]/, args.type);
481         message = message.replace(/\[2\]/, this.report.users[args.properties.userid]);
483         this.display_submission_error(message, args.properties.cell);
484         return;
485     }
487     // Quick reference for the grader report
488     var i = null;
489     // Check the outcome
490     if (outcome.result == 'success') {
491         // Iterate through each row in the result object
492         for (i in outcome.row) {
493             if (outcome.row[i] && outcome.row[i].userid && outcome.row[i].itemid) {
494                 // alias it, we use it quite a bit
495                 var r = outcome.row[i];
496                 // Get the cell referred to by this result object
497                 var info = this.report.get_cell_info([r.userid, r.itemid]);
498                 if (!info) {
499                     continue;
500                 }
501                 // Calculate the final grade for the cell
502                 var finalgrade = '';
503                 var scalegrade = -1;
504                 if (!r.finalgrade) {
505                     if (this.report.isediting) {
506                         // In edit mode don't put hyphens in the grade text boxes
507                         finalgrade = '';
508                     } else {
509                         // In non-edit mode put a hyphen in the grade cell
510                         finalgrade = '-';
511                     }
512                 } else {
513                     if (r.scale) {
514                         scalegrade = parseFloat(r.finalgrade);
515                         finalgrade = this.scales[r.scale][scalegrade-1];
516                     } else {
517                         finalgrade = parseFloat(r.finalgrade).toFixed(info.itemdp);
518                     }
519                 }
520                 if (this.report.isediting) {
521                     var grade = info.cell.one('#grade_'+r.userid+'_'+r.itemid);
522                     if (grade) {
523                         // This means the item has a input element to update.
524                         var parent = grade.ancestor('td');
525                         if (parent.hasClass('grade_type_scale')) {
526                             grade.all('option').each(function(option) {
527                                 if (option.get('value') == scalegrade) {
528                                     option.setAttribute('selected', 'selected');
529                                 } else {
530                                     option.removeAttribute('selected');
531                                 }
532                             });
533                         } else {
534                             grade.set('value', finalgrade);
535                         }
536                     } else if (info.cell.one('.gradevalue')) {
537                         // This means we are updating a value for something without editing boxed (locked, etc).
538                         info.cell.one('.gradevalue').set('innerHTML', finalgrade);
539                     }
540                 } else {
541                     // If there is no currently editing field or if this cell is not being currently edited
542                     if (!this.current || info.cell.get('id') != this.current.node.get('id')) {
543                         // Update the value
544                         var node = info.cell.one('.gradevalue');
545                         var td = node.ancestor('td');
546                         // Only scale and value type grades should have their content updated in this way.
547                         if (td.hasClass('grade_type_value') || td.hasClass('grade_type_scale')) {
548                             node.set('innerHTML', finalgrade);
549                         }
550                     } else if (this.current && info.cell.get('id') == this.current.node.get('id')) {
551                         // If we are here the grade value of the cell currently being edited has changed !!!!!!!!!
552                         // If the user has not actually changed the old value yet we will automatically correct it
553                         // otherwise we will prompt the user to choose to use their value or the new value!
554                         if (!this.current.has_changed() || confirm(M.util.get_string('ajaxfieldchanged', 'gradereport_grader'))) {
555                             this.current.set_grade(finalgrade);
556                             if (this.current.grade) {
557                                 this.current.grade.set('value', finalgrade);
558                             }
559                         }
560                     }
561                 }
562             }
563         }
564         // Flag the changed cell as overridden by ajax
565         args.properties.cell.addClass('ajaxoverridden');
566     } else {
567         var p = args.properties;
568         if (args.type == 'grade') {
569             var oldgrade = args.values.oldgrade;
570             p.cell.one('.gradevalue').set('innerHTML',oldgrade);
571         } else if (args.type == 'feedback') {
572             this.report.update_feedback(p.userid, p.itemid, args.values.oldfeedback);
573         }
574         this.display_submission_error(outcome.message, p.cell);
575     }
576     // Check if all IO transactions in the queue are complete yet
577     var allcomplete = true;
578     for (i in this.pendingsubmissions) {
579         if (this.pendingsubmissions[i]) {
580             if (this.pendingsubmissions[i].transaction.id == tid) {
581                 this.pendingsubmissions[i].complete = true;
582                 this.pendingsubmissions[i].outcome = outcome;
583                 this.report.Y.io.queue.remove(this.pendingsubmissions[i].transaction);
584             }
585             if (!this.pendingsubmissions[i].complete) {
586                 allcomplete = false;
587             }
588         }
589     }
590     if (allcomplete) {
591         this.pendingsubmissions = [];
592     }
593 };
594 /**
595  * Displays a submission error within a overlay on the cell that failed update
596  *
597  * @function
598  * @this {M.gradereport_grader.classes.ajax}
599  * @param {String} message
600  * @param {Y.Node} cell
601  */
602 M.gradereport_grader.classes.ajax.prototype.display_submission_error = function(message, cell) {
603     var erroroverlay = new this.report.Y.Overlay({
604         headerContent : '<div><strong class="error">'+M.util.get_string('ajaxerror', 'gradereport_grader')+'</strong>  <em>'+M.util.get_string('ajaxclicktoclose', 'gradereport_grader')+'</em></div>',
605         bodyContent : message,
606         visible : false,
607         zIndex : 3
608     });
609     erroroverlay.set('xy', [cell.getX()+10,cell.getY()+10]);
610     erroroverlay.render(this.report.table.ancestor('div'));
611     erroroverlay.show();
612     erroroverlay.get('boundingBox').on('click', function(){
613         this.get('boundingBox').setStyle('visibility', 'hidden');
614         this.hide();
615         this.destroy();
616     }, erroroverlay);
617     erroroverlay.get('boundingBox').setStyle('visibility', 'visible');
618 };
619 /**
620  * A class for existing fields
621  * This class is used only when the user is in editing mode
622  *
623  * @class existingfield
624  * @constructor
625  * @param {M.gradereport_grader.classes.report} report
626  * @param {Int} userid
627  * @param {Int} itemid
628  */
629 M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
630     this.report = ajax.report;
631     this.userid = userid;
632     this.itemid = itemid;
633     this.editfeedback = ajax.showquickfeedback;
634     this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid);
636     var i = 0;
637     if (this.grade) {
638         for (i = 0; i < this.report.grades.length; i++) {
639             if (this.report.grades[i]['user'] == this.userid && this.report.grades[i]['item'] == this.itemid) {
640                 this.oldgrade = this.report.grades[i]['grade'];
641             }
642         }
644         if (!this.oldgrade) {
645             // Assigning an empty string makes determining whether the grade has been changed easier
646             // This value is never sent to the server
647             this.oldgrade = '';
648         }
650         // On blur save any changes in the grade field
651         this.grade.on('blur', this.submit, this);
652     }
654     // Check if feedback is enabled
655     if (this.editfeedback) {
656         // Get the feedback fields
657         this.feedback = this.report.Y.one('#feedback_'+userid+'_'+itemid);
659         if (this.feedback) {
660             for(i = 0; i < this.report.feedback.length; i++) {
661                 if (this.report.feedback[i]['user'] == this.userid && this.report.feedback[i]['item'] == this.itemid) {
662                     this.oldfeedback = this.report.feedback[i]['content'];
663                 }
664             }
666             if(!this.oldfeedback) {
667                 // Assigning an empty string makes determining whether the feedback has been changed easier
668                 // This value is never sent to the server
669                 this.oldfeedback = '';
670             }
672             // On blur save any changes in the feedback field
673             this.feedback.on('blur', this.submit, this);
675             // Override the default tab movements when moving between cells
676             // Handle Tab.
677             this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.feedback, 'press:9', this, true));
678             // Handle the Enter key being pressed.
679             this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.feedback, 'press:13', this));
680             // Handle CTRL + arrow keys.
681             this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.feedback, 'press:37,38,39,40+ctrl', this));
683             if (this.grade) {
684                 // Override the default tab movements when moving between cells
685                 // Handle Shift+Tab.
686                 this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9+shift', this));
688                 // Override the default tab movements for fields in the same cell
689                 this.keyevents.push(this.report.Y.on('key',
690                         function(e){e.preventDefault();this.grade.focus();},
691                         this.feedback,
692                         'press:9+shift',
693                         this));
694                 this.keyevents.push(this.report.Y.on('key',
695                         function(e){if (e.shiftKey) {return;}e.preventDefault();this.feedback.focus();},
696                         this.grade,
697                         'press:9',
698                         this));
699             }
700         }
701     } else if (this.grade) {
702         // Handle Tab and Shift+Tab.
703         this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this));
704     }
705     if (this.grade) {
706         // Handle the Enter key being pressed.
707         this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'press:13', this));
708         // Handle CTRL + arrow keys.
709         this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'press:37,38,39,40+ctrl', this));
710     }
711 };
712 /**
713  * Attach the required properties and methods to the existing field class
714  * via prototyping
715  */
716 M.gradereport_grader.classes.existingfield.prototype.userid = null;
717 M.gradereport_grader.classes.existingfield.prototype.itemid = null;
718 M.gradereport_grader.classes.existingfield.prototype.editfeedback = false;
719 M.gradereport_grader.classes.existingfield.prototype.grade = null;
720 M.gradereport_grader.classes.existingfield.prototype.oldgrade = null;
721 M.gradereport_grader.classes.existingfield.prototype.keyevents = [];
722 /**
723  * Handles saving of changed on keypress
724  *
725  * @function
726  * @this {M.gradereport_grader.classes.existingfield}
727  * @param {Event} e
728  */
729 M.gradereport_grader.classes.existingfield.prototype.keypress_enter = function(e) {
730     e.preventDefault();
731     this.submit();
732 };
733 /**
734  * Handles setting the correct focus if the user presses tab
735  *
736  * @function
737  * @this {M.gradereport_grader.classes.existingfield}
738  * @param {Event} e
739  * @param {Bool} ignoreshift
740  */
741 M.gradereport_grader.classes.existingfield.prototype.keypress_tab = function(e, ignoreshift) {
742     e.preventDefault();
743     var next = null;
744     if (e.shiftKey) {
745         if (ignoreshift) {
746             return;
747         }
748         next = this.report.ajax.get_above_cell(this.grade.ancestor('td'));
749     } else {
750         next = this.report.ajax.get_below_cell(this.grade.ancestor('td'));
751     }
752     this.move_focus(next);
753 };
754 /**
755  * Handles setting the correct focus when the user presses CTRL+arrow keys
756  *
757  * @function
758  * @this {M.gradereport_grader.classes.existingfield}
759  * @param {Event} e
760  */
761 M.gradereport_grader.classes.existingfield.prototype.keypress_arrows = function(e) {
762     var next = null;
763     switch (e.keyCode) {
764         case 37:    // Left
765             next = this.report.ajax.get_prev_cell(this.grade.ancestor('td'));
766             break;
767         case 38:    // Up
768             next = this.report.ajax.get_above_cell(this.grade.ancestor('td'));
769             break;
770         case 39:    // Right
771             next = this.report.ajax.get_next_cell(this.grade.ancestor('td'));
772             break;
773         case 40:    // Down
774             next = this.report.ajax.get_below_cell(this.grade.ancestor('td'));
775             break;
776     }
777     this.move_focus(next);
778 };
779 /**
780  * Move the focus to the node
781  * @function
782  * @this {M.gradereport_grader.classes.existingfield}
783  * @param {Y.Node} node
784  */
785 M.gradereport_grader.classes.existingfield.prototype.move_focus = function(node) {
786     if (node) {
787         var properties = this.report.get_cell_info(node);
788         switch(properties.itemtype) {
789             case 'scale':
790                 properties.cell.one('select.select').focus();
791                 break;
792             case 'value':
793             default:
794                 properties.cell.one('input.text').focus();
795                 break;
796         }
797     }
798 };
799 /**
800  * Checks if the values for the field have changed
801  *
802  * @function
803  * @this {M.gradereport_grader.classes.existingfield}
804  * @return {Bool}
805  */
806 M.gradereport_grader.classes.existingfield.prototype.has_changed = function() {
807     if (this.grade) {
808         if (this.grade.get('value') !== this.oldgrade) {
809             return true;
810         }
811     }
812     if (this.editfeedback && this.feedback) {
813         if (this.feedback.get('value') !== this.oldfeedback) {
814             return true;
815         }
816     }
817     return false;
818 };
819 /**
820  * Submits any changes and then updates the fields accordingly
821  *
822  * @function
823  * @this {M.gradereport_grader.classes.existingfield}
824  */
825 M.gradereport_grader.classes.existingfield.prototype.submit = function() {
826     if (!this.has_changed()) {
827         return;
828     }
830     var properties = this.report.get_cell_info([this.userid,this.itemid]);
831     var values = (function(f){
832         var feedback, oldfeedback, grade, oldgrade = null;
833         if (f.editfeedback && f.feedback) {
834             feedback = f.feedback.get('value');
835             oldfeedback = f.oldfeedback;
836         }
837         if (f.grade) {
838             grade = f.grade.get('value');
839             oldgrade = f.oldgrade;
840         }
841         return {
842             editablefeedback : f.editfeedback,
843             grade : grade,
844             oldgrade : oldgrade,
845             feedback : feedback,
846             oldfeedback : oldfeedback
847         };
848     })(this);
850     this.oldgrade = values.grade;
851     if (values.editablefeedback && values.feedback != values.oldfeedback) {
852         this.report.update_feedback(this.userid, this.itemid, values.feedback);
853         this.oldfeedback = values.feedback;
854     }
856     this.report.ajax.submit(properties, values);
857 };
859 /**
860  * Textfield class
861  * This classes gets used in conjunction with the report running with AJAX enabled
862  * and is used to manage a cell that has a grade requiring a textfield for input
863  *
864  * @class textfield
865  * @constructor
866  * @this {M.gradereport_grader.classes.textfield}
867  * @param {M.gradereport_grader.classes.report} report
868  * @param {Y.Node} node
869  */
870 M.gradereport_grader.classes.textfield = function(report, node) {
871     this.report = report;
872     this.node = node;
873     this.gradespan = node.one('.gradevalue');
874     this.inputdiv = this.report.Y.Node.create('<div></div>');
875     this.editfeedback = this.report.ajax.showquickfeedback;
876     this.grade = this.report.Y.Node.create('<input type="text" class="text" value="" name="ajaxgrade" />');
877     this.gradetype = 'value';
878     this.inputdiv.append(this.grade);
879     if (this.report.ajax.showquickfeedback) {
880         this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback" />');
881         this.inputdiv.append(this.feedback);
882     }
883 };
884 /**
885  * Extend the textfield class with the following methods and properties
886  */
887 M.gradereport_grader.classes.textfield.prototype.keyevents = [];
888 M.gradereport_grader.classes.textfield.prototype.editable = false;
889 M.gradereport_grader.classes.textfield.prototype.gradetype = null;
890 M.gradereport_grader.classes.textfield.prototype.grade = null;
891 M.gradereport_grader.classes.textfield.prototype.report = null;
892 M.gradereport_grader.classes.textfield.prototype.node = null;
893 M.gradereport_grader.classes.textfield.prototype.gradespam = null;
894 M.gradereport_grader.classes.textfield.prototype.inputdiv = null;
895 M.gradereport_grader.classes.textfield.prototype.editfeedback = false;
896 /**
897  * Replaces the cell contents with the controls to enable editing
898  *
899  * @function
900  * @this {M.gradereport_grader.classes.textfield}
901  * @return {M.gradereport_grader.classes.textfield}
902  */
903 M.gradereport_grader.classes.textfield.prototype.replace = function() {
904     this.set_grade(this.get_grade());
905     if (this.editfeedback) {
906         this.set_feedback(this.get_feedback());
907     }
908     this.node.replaceChild(this.inputdiv, this.gradespan);
909     if (this.grade) {
910         this.grade.focus();
911     } else if (this.feedback) {
912         this.feedback.focus();
913     }
914     this.editable = true;
915     return this;
916 };
917 /**
918  * Commits the changes within a cell and returns a result object of new + old values
919  * @function
920  * @this {M.gradereport_grader.classes.textfield}
921  * @return {Object}
922  */
923 M.gradereport_grader.classes.textfield.prototype.commit = function() {
924     // Produce an anonymous result object contianing all values
925     var result = (function(field){
926         // Editable false lets us get the pre-update values.
927         field.editable = false;
928         var oldgrade = field.get_grade();
929         if (oldgrade == '-') {
930             oldgrade = '';
931         }
932         var feedback = null;
933         var oldfeedback = null;
934         if (field.editfeedback) {
935             oldfeedback = field.get_feedback();
936         }
938         // Now back to editable gives us the values in the edit areas.
939         field.editable = true;
940         if (field.editfeedback) {
941             feedback = field.get_feedback();
942         }
943         return {
944             gradetype : field.gradetype,
945             editablefeedback : field.editfeedback,
946             grade : field.get_grade(),
947             oldgrade : oldgrade,
948             feedback : feedback,
949             oldfeedback : oldfeedback
950         };
951     })(this);
952     // Set the changes in stone
953     this.set_grade(result.grade);
954     if (this.editfeedback) {
955         this.set_feedback(result.feedback);
956     }
957     // Return the result object
958     return result;
959 };
960 /**
961  * Reverts a cell back to its static contents
962  * @function
963  * @this {M.gradereport_grader.classes.textfield}
964  */
965 M.gradereport_grader.classes.textfield.prototype.revert = function() {
966     this.node.replaceChild(this.gradespan, this.inputdiv);
967     for (var i in this.keyevents) {
968         if (this.keyevents[i]) {
969             this.keyevents[i].detach();
970         }
971     }
972     this.keyevents = [];
973     this.node.on('click', this.report.ajax.make_editable, this.report.ajax);
974 };
975 /**
976  * Gets the grade for current cell
977  *
978  * @function
979  * @this {M.gradereport_grader.classes.textfield}
980  * @return {Mixed}
981  */
982 M.gradereport_grader.classes.textfield.prototype.get_grade = function() {
983     if (this.editable) {
984         return this.grade.get('value');
985     }
986     return this.gradespan.get('innerHTML');
987 };
988 /**
989  * Sets the grade for the current cell
990  * @function
991  * @this {M.gradereport_grader.classes.textfield}
992  * @param {Mixed} value
993  */
994 M.gradereport_grader.classes.textfield.prototype.set_grade = function(value) {
995     if (!this.editable) {
996         if (value == '-') {
997             value = '';
998         }
999         this.grade.set('value', value);
1000     } else {
1001         if (value == '') {
1002             value = '-';
1003         }
1004         this.gradespan.set('innerHTML', value);
1005     }
1006 };
1007 /**
1008  * Gets the feedback for the current cell
1009  * @function
1010  * @this {M.gradereport_grader.classes.textfield}
1011  * @return {String}
1012  */
1013 M.gradereport_grader.classes.textfield.prototype.get_feedback = function() {
1014     if (this.editable) {
1015         if (this.feedback) {
1016             return this.feedback.get('value');
1017         } else {
1018             return null;
1019         }
1020     }
1021     var properties = this.report.get_cell_info(this.node);
1022     if (properties) {
1023         return properties.feedback;
1024     }
1025     return '';
1026 };
1027 /**
1028  * Sets the feedback for the current cell
1029  * @function
1030  * @this {M.gradereport_grader.classes.textfield}
1031  * @param {Mixed} value
1032  */
1033 M.gradereport_grader.classes.textfield.prototype.set_feedback = function(value) {
1034     if (!this.editable) {
1035         if (this.feedback) {
1036             this.feedback.set('value', value);
1037         }
1038     } else {
1039         var properties = this.report.get_cell_info(this.node);
1040         this.report.update_feedback(properties.userid, properties.itemid, value);
1041     }
1042 };
1043 /**
1044  * Checks if the current cell has changed at all
1045  * @function
1046  * @this {M.gradereport_grader.classes.textfield}
1047  * @return {Bool}
1048  */
1049 M.gradereport_grader.classes.textfield.prototype.has_changed = function() {
1050     // If its not editable it has not changed
1051     if (!this.editable) {
1052         return false;
1053     }
1054     // If feedback is being edited then it has changed if either grade or feedback have changed
1055     if (this.editfeedback) {
1056         var properties = this.report.get_cell_info(this.node);
1057         if (this.get_feedback() != properties.feedback) {
1058             return true;
1059         }
1060     }
1062     if (this.grade) {
1063         return (this.get_grade() != this.gradespan.get('innerHTML'));
1064     } else {
1065         return false;
1066     }
1067 };
1068 /**
1069  * Attaches the key listeners for the editable fields and stored the event references
1070  * against the textfield
1071  *
1072  * @function
1073  * @this {M.gradereport_grader.classes.textfield}
1074  */
1075 M.gradereport_grader.classes.textfield.prototype.attach_key_events = function() {
1076     var a = this.report.ajax;
1077     // Setup the default key events for tab and enter
1078     if (this.editfeedback) {
1079         if (this.grade) {
1080             // Handle Shift+Tab.
1081             this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9+shift', a));
1082         }
1083         // Handle Tab.
1084         this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'press:9', a, true));
1085         // Handle the Enter key being pressed.
1086         this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'press:13', a));
1087     } else {
1088         if (this.grade) {
1089             // Handle Tab and Shift+Tab.
1090             this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9', a));
1091         }
1092     }
1094     // Setup the arrow key events.
1095     // Handle CTRL + arrow keys.
1096     this.keyevents.push(this.report.Y.on('key', a.keypress_arrows, this.inputdiv.ancestor('td'), 'down:37,38,39,40+ctrl', a));
1098     if (this.grade) {
1099         // Handle the Enter key being pressed.
1100         this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'press:13', a));
1101         // Prevent the default key action on all fields for arrow keys on all key events!
1102         // Note: this still does not work in FF!!!!!
1103         this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'down:37,38,39,40+ctrl'));
1104         this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'press:37,38,39,40+ctrl'));
1105         this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'up:37,38,39,40+ctrl'));
1106     }
1107 };
1109 /**
1110  * Feedback field class
1111  * This classes gets used in conjunction with the report running with AJAX enabled
1112  * and is used to manage a cell that no editable grade, only possibly feedback
1113  *
1114  * @class feedbackfield
1115  * @constructor
1116  * @this {M.gradereport_grader.classes.feedbackfield}
1117  * @param {M.gradereport_grader.classes.report} report
1118  * @param {Y.Node} node
1119  */
1120 M.gradereport_grader.classes.feedbackfield = function(report, node) {
1121     this.report = report;
1122     this.node = node;
1123     this.gradespan = node.one('.gradevalue');
1124     this.inputdiv = this.report.Y.Node.create('<div></div>');
1125     this.editfeedback = this.report.ajax.showquickfeedback;
1126     this.gradetype = 'text';
1127     if (this.report.ajax.showquickfeedback) {
1128         this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback" />');
1129         this.inputdiv.append(this.feedback);
1130     }
1131 };
1133 /**
1134  * Gets the grade for current cell (which will always be null)
1135  *
1136  * @function
1137  * @this {M.gradereport_grader.classes.feedbackfield}
1138  * @return {Mixed}
1139  */
1140 M.gradereport_grader.classes.feedbackfield.prototype.get_grade = function() {
1141     return null;
1142 };
1144 /**
1145  * Overrides the set_grade function of textfield so that it can ignore the set-grade
1146  * for grade cells without grades
1147  *
1148  * @function
1149  * @this {M.gradereport_grader.classes.feedbackfield}
1150  * @param {String} value
1151  */
1152 M.gradereport_grader.classes.feedbackfield.prototype.set_grade = function() {
1153     return;
1154 };
1156 /**
1157  * Manually extend the feedbackfield class with the properties and methods of the
1158  * textfield class that have not been defined
1159  */
1160 for (var i in M.gradereport_grader.classes.textfield.prototype) {
1161     if (!M.gradereport_grader.classes.feedbackfield.prototype[i]) {
1162         M.gradereport_grader.classes.feedbackfield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i];
1163     }
1166 /**
1167  * An editable scale field
1168  *
1169  * @class scalefield
1170  * @constructor
1171  * @inherits M.gradereport_grader.classes.textfield
1172  * @base M.gradereport_grader.classes.textfield
1173  * @this {M.gradereport_grader.classes.scalefield}
1174  * @param {M.gradereport_grader.classes.report} report
1175  * @param {Y.Node} node
1176  */
1177 M.gradereport_grader.classes.scalefield = function(report, node) {
1178     this.report = report;
1179     this.node = node;
1180     this.gradespan = node.one('.gradevalue');
1181     this.inputdiv = this.report.Y.Node.create('<div></div>');
1182     this.editfeedback = this.report.ajax.showquickfeedback;
1183     this.grade = this.report.Y.Node.create('<select type="text" class="text" name="ajaxgrade" /><option value="-1">'+
1184             M.util.get_string('ajaxchoosescale', 'gradereport_grader')+'</option></select>');
1185     this.gradetype = 'scale';
1186     this.inputdiv.append(this.grade);
1187     if (this.editfeedback) {
1188         this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback"/>');
1189         this.inputdiv.append(this.feedback);
1190     }
1191     var properties = this.report.get_cell_info(node);
1192     this.scale = this.report.ajax.scales[properties.itemscale];
1193     for (var i in this.scale) {
1194         if (this.scale[i]) {
1195             this.grade.append(this.report.Y.Node.create('<option value="'+(parseFloat(i)+1)+'">'+this.scale[i]+'</option>'));
1196         }
1197     }
1198 };
1199 /**
1200  * Override + extend the scalefield class with the following properties
1201  * and methods
1202  */
1203 /**
1204  * @property {Array} scale
1205  */
1206 M.gradereport_grader.classes.scalefield.prototype.scale = [];
1207 /**
1208  * Extend the scalefield with the functions from the textfield
1209  */
1210 /**
1211  * Overrides the get_grade function so that it can pick up the value from the
1212  * scales select box
1213  *
1214  * @function
1215  * @this {M.gradereport_grader.classes.scalefield}
1216  * @return {Int} the scale id
1217  */
1218 M.gradereport_grader.classes.scalefield.prototype.get_grade = function(){
1219     if (this.editable) {
1220         // Return the scale value
1221         return this.grade.all('option').item(this.grade.get('selectedIndex')).get('value');
1222     } else {
1223         // Return the scale values id
1224         var value = this.gradespan.get('innerHTML');
1225         for (var i in this.scale) {
1226             if (this.scale[i] == value) {
1227                 return parseFloat(i)+1;
1228             }
1229         }
1230         return -1;
1231     }
1232 };
1233 /**
1234  * Overrides the set_grade function of textfield so that it can set the scale
1235  * within the scale select box
1236  *
1237  * @function
1238  * @this {M.gradereport_grader.classes.scalefield}
1239  * @param {String} value
1240  */
1241 M.gradereport_grader.classes.scalefield.prototype.set_grade = function(value) {
1242     if (!this.editable) {
1243         if (value == '-') {
1244             value = '-1';
1245         }
1246         this.grade.all('option').each(function(node){
1247             if (node.get('value') == value) {
1248                 node.set('selected', true);
1249             }
1250         });
1251     } else {
1252         if (value == '' || value == '-1') {
1253             value = '-';
1254         } else {
1255             value = this.scale[parseFloat(value)-1];
1256         }
1257         this.gradespan.set('innerHTML', value);
1258     }
1259 };
1260 /**
1261  * Checks if the current cell has changed at all
1262  * @function
1263  * @this {M.gradereport_grader.classes.scalefield}
1264  * @return {Bool}
1265  */
1266 M.gradereport_grader.classes.scalefield.prototype.has_changed = function() {
1267     if (!this.editable) {
1268         return false;
1269     }
1270     var gradef = this.get_grade();
1271     this.editable = false;
1272     var gradec = this.get_grade();
1273     this.editable = true;
1274     if (this.editfeedback) {
1275         var properties = this.report.get_cell_info(this.node);
1276         var feedback = properties.feedback;
1277         return (gradef != gradec || this.get_feedback() != feedback);
1278     }
1279     return (gradef != gradec);
1280 };
1282 /**
1283  * Manually extend the scalefield class with the properties and methods of the
1284  * textfield class that have not been defined
1285  */
1286 for (var i in M.gradereport_grader.classes.textfield.prototype) {
1287     if (!M.gradereport_grader.classes.scalefield.prototype[i]) {
1288         M.gradereport_grader.classes.scalefield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i];
1289     }