7bf49d2be81a7fd8ccbada3a04770fb4dfeee264
[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 = 'text';
213     if (node.hasClass('grade_type_scale')) {
214         fieldtype = 'scale';
215     }
216     // Create the appropriate field widget
217     switch (fieldtype) {
218         case 'scale':
219             this.current = new M.gradereport_grader.classes.scalefield(this.report, node);
220             break;
221         case 'text':
222         default:
223             this.current = new M.gradereport_grader.classes.textfield(this.report, node);
224             break;
225     }
226     this.current.replace().attach_key_events();
228     // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
229     Y.Global.fire('moodle-gradereport_grader:resized');
230 };
231 /**
232  * Callback function for the user pressing the enter key on an editable field
233  *
234  * @function
235  * @this {M.gradereport_grader.classes.ajax}
236  * @param {Event} e
237  */
238 M.gradereport_grader.classes.ajax.prototype.keypress_enter = function(e) {
239     this.process_editable_field(null);
240 };
241 /**
242  * Callback function for the user pressing Tab or Shift+Tab
243  *
244  * @function
245  * @this {M.gradereport_grader.classes.ajax}
246  * @param {Event} e
247  * @param {Bool} ignoreshift If true and shift is pressed then don't exec
248  */
249 M.gradereport_grader.classes.ajax.prototype.keypress_tab = function(e, ignoreshift) {
250     var next = null;
251     if (e.shiftKey) {
252         if (ignoreshift) {
253             return;
254         }
255         next = this.get_above_cell();
256     } else {
257         next = this.get_below_cell();
258     }
259     this.process_editable_field(next);
260 };
261 /**
262  * Callback function for the user pressing an CTRL + an arrow key
263  *
264  * @function
265  * @this {M.gradereport_grader.classes.ajax}
266  */
267 M.gradereport_grader.classes.ajax.prototype.keypress_arrows = function(e) {
268     e.preventDefault();
269     var next = null;
270     switch (e.keyCode) {
271         case 37:    // Left
272             next = this.get_prev_cell();
273             break;
274         case 38:    // Up
275             next = this.get_above_cell();
276             break;
277         case 39:    // Right
278             next = this.get_next_cell();
279             break;
280         case 40:    // Down
281             next = this.get_below_cell();
282             break;
283     }
284     this.process_editable_field(next);
285 };
286 /**
287  * Processes an editable field an does what ever is required to update it
288  *
289  * @function
290  * @this {M.gradereport_grader.classes.ajax}
291  * @param {Y.Node|null} next The next node to make editable (chaining)
292  */
293 M.gradereport_grader.classes.ajax.prototype.process_editable_field = function(next) {
294     if (this.current.has_changed()) {
295         var properties = this.report.get_cell_info(this.current.node);
296         var values = this.current.commit();
297         this.current.revert();
298         this.submit(properties, values);
299     } else {
300         this.current.revert();
301     }
302     this.current = null;
303     if (next) {
304         this.make_editable(next, null);
305     }
307     // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
308     Y.Global.fire('moodle-gradereport_grader:resized');
309 };
310 /**
311  * Gets the next cell that is editable (right)
312  * @function
313  * @this {M.gradereport_grader.classes.ajax}
314  * @param {Y.Node} cell
315  * @return {Y.Node}
316  */
317 M.gradereport_grader.classes.ajax.prototype.get_next_cell = function(cell) {
318     var n = cell || this.current.node;
319     var next = n.next('td');
320     var tr = null;
321     if (!next && (tr = n.ancestor('tr').next('tr'))) {
322         next = tr.all('.grade').item(0);
323     }
324     if (!next) {
325         next = this.current.node;
326     }
327     return next;
328 };
329 /**
330  * Gets the previous cell that is editable (left)
331  * @function
332  * @this {M.gradereport_grader.classes.ajax}
333  * @param {Y.Node} cell
334  * @return {Y.Node}
335  */
336 M.gradereport_grader.classes.ajax.prototype.get_prev_cell = function(cell) {
337     var n = cell || this.current.node;
338     var next = n.previous('.grade');
339     var tr = null;
340     if (!next && (tr = n.ancestor('tr').previous('tr'))) {
341         var cells = tr.all('.grade');
342         next = cells.item(cells.size()-1);
343     }
344     if (!next) {
345         next = this.current.node;
346     }
347     return next;
348 };
349 /**
350  * Gets the cell above if it is editable (up)
351  * @function
352  * @this {M.gradereport_grader.classes.ajax}
353  * @param {Y.Node} cell
354  * @return {Y.Node}
355  */
356 M.gradereport_grader.classes.ajax.prototype.get_above_cell = function(cell) {
357     var n = cell || this.current.node;
358     var tr = n.ancestor('tr').previous('tr');
359     var next = null;
360     if (tr) {
361         var column = 0;
362         var ntemp = n;
363         while (ntemp = ntemp.previous('td.cell')) {
364             column++;
365         }
366         next = tr.all('td.cell').item(column);
367     }
368     if (!next) {
369         next = this.current.node;
370     }
371     return next;
372 };
373 /**
374  * Gets the cell below if it is editable (down)
375  * @function
376  * @this {M.gradereport_grader.classes.ajax}
377  * @param {Y.Node} cell
378  * @return {Y.Node}
379  */
380 M.gradereport_grader.classes.ajax.prototype.get_below_cell = function(cell) {
381     var n = cell || this.current.node;
382     var tr = n.ancestor('tr').next('tr');
383     var next = null;
384     if (tr && !tr.hasClass('avg')) {
385         var column = 0;
386         var ntemp = n;
387         while (ntemp = ntemp.previous('td.cell')) {
388             column++;
389         }
390         next = tr.all('td.cell').item(column);
391     }
392     // next will be null when we get to the bottom of a column
393     return next;
394 };
395 /**
396  * Submits changes for update
397  *
398  * @function
399  * @this {M.gradereport_grader.classes.ajax}
400  * @param {Object} properties Properties of the cell being edited
401  * @param {Object} values Object containing old + new values
402  */
403 M.gradereport_grader.classes.ajax.prototype.submit = function(properties, values) {
404     // Stop the IO queue so we can add to it
405     this.report.Y.io.queue.stop();
406     // If the grade has changed add an IO transaction to update it to the queue
407     if (values.grade !== values.oldgrade) {
408         this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', {
409             method : 'POST',
410             data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.grade+'&type='+properties.itemtype+'&sesskey='+M.cfg.sesskey,
411             on : {
412                 complete : this.submission_outcome
413             },
414             context : this,
415             arguments : {
416                 properties : properties,
417                 values : values,
418                 type : 'grade'
419             }
420         }),complete:false,outcome:null});
421     }
422     // If feedback is editable and has changed add to the IO queue for it
423     if (values.editablefeedback && values.feedback !== values.oldfeedback) {
424         this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', {
425             method : 'POST',
426             data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.feedback+'&type=feedback&sesskey='+M.cfg.sesskey,
427             on : {
428                 complete : this.submission_outcome
429             },
430             context : this,
431             arguments : {
432                 properties : properties,
433                 values : values,
434                 type : 'feedback'
435             }
436         }),complete:false,outcome:null});
437     }
438     // Process the IO queue
439     this.report.Y.io.queue.start();
440 };
441 /**
442  * Callback function for IO transaction completions
443  *
444  * Uses a synchronous queue to ensure we maintain some sort of order
445  *
446  * @function
447  * @this {M.gradereport_grader.classes.ajax}
448  * @param {Int} tid Transaction ID
449  * @param {Object} outcome
450  * @param {Mixed} args
451  */
452 M.gradereport_grader.classes.ajax.prototype.submission_outcome = function(tid, outcome, args) {
453     // Parse the response as JSON
454     try {
455         outcome = this.report.Y.JSON.parse(outcome.responseText);
456     } catch(e) {
457         var message = M.util.get_string('ajaxfailedupdate', 'gradereport_grader');
458         message = message.replace(/\[1\]/, args.type);
459         message = message.replace(/\[2\]/, this.report.users[args.properties.userid]);
461         this.display_submission_error(message, args.properties.cell);
462         return;
463     }
465     // Quick reference for the grader report
466     var i = null;
467     // Check the outcome
468     if (outcome.result == 'success') {
469         // Iterate through each row in the result object
470         for (i in outcome.row) {
471             if (outcome.row[i] && outcome.row[i].userid && outcome.row[i].itemid) {
472                 // alias it, we use it quite a bit
473                 var r = outcome.row[i];
474                 // Get the cell referred to by this result object
475                 var info = this.report.get_cell_info([r.userid, r.itemid]);
476                 if (!info) {
477                     continue;
478                 }
479                 // Calculate the final grade for the cell
480                 var finalgrade = '';
481                 if (!r.finalgrade) {
482                     if (this.report.isediting) {
483                         // In edit mode don't put hyphens in the grade text boxes
484                         finalgrade = '';
485                     } else {
486                         // In non-edit mode put a hyphen in the grade cell
487                         finalgrade = '-';
488                     }
489                 } else {
490                     if (r.scale) {
491                         finalgrade = this.scales[r.scale][parseFloat(r.finalgrade)-1];
492                     } else {
493                         finalgrade = parseFloat(r.finalgrade).toFixed(info.itemdp);
494                     }
495                 }
496                 if (this.report.isediting) {
497                     if (args.properties.itemtype == 'scale') {
498                         info.cell.one('#grade_'+r.userid+'_'+r.itemid).all('options').each(function(option){
499                             if (option.get('value') == finalgrade) {
500                                 option.setAttribute('selected', 'selected');
501                             } else {
502                                 option.removeAttribute('selected');
503                             }
504                         });
505                     } else {
506                         info.cell.one('#grade_'+r.userid+'_'+r.itemid).set('value', finalgrade);
507                     }
508                 } else {
509                     // If there is no currently editing field or if this cell is not being currently edited
510                     if (!this.current || info.cell.get('id') != this.current.node.get('id')) {
511                         // Update the value
512                         info.cell.one('.gradevalue').set('innerHTML',finalgrade);
513                     } else if (this.current && info.cell.get('id') == this.current.node.get('id')) {
514                         // If we are here the grade value of the cell currently being edited has changed !!!!!!!!!
515                         // If the user has not actually changed the old value yet we will automatically correct it
516                         // otherwise we will prompt the user to choose to use their value or the new value!
517                         if (!this.current.has_changed() || confirm(M.util.get_string('ajaxfieldchanged', 'gradereport_grader'))) {
518                             this.current.set_grade(finalgrade);
519                             this.current.grade.set('value', finalgrade);
520                         }
521                     }
522                 }
523             }
524         }
525         // Flag the changed cell as overridden by ajax
526         args.properties.cell.addClass('ajaxoverridden');
527     } else {
528         var p = args.properties;
529         if (args.type == 'grade') {
530             var oldgrade = args.values.oldgrade;
531             p.cell.one('.gradevalue').set('innerHTML',oldgrade);
532         } else if (args.type == 'feedback') {
533             this.report.update_feedback(p.userid, p.itemid, args.values.oldfeedback);
534         }
535         this.display_submission_error(outcome.message, p.cell);
536     }
537     // Check if all IO transactions in the queue are complete yet
538     var allcomplete = true;
539     for (i in this.pendingsubmissions) {
540         if (this.pendingsubmissions[i]) {
541             if (this.pendingsubmissions[i].transaction.id == tid) {
542                 this.pendingsubmissions[i].complete = true;
543                 this.pendingsubmissions[i].outcome = outcome;
544                 this.report.Y.io.queue.remove(this.pendingsubmissions[i].transaction);
545             }
546             if (!this.pendingsubmissions[i].complete) {
547                 allcomplete = false;
548             }
549         }
550     }
551     if (allcomplete) {
552         this.pendingsubmissions = [];
553     }
554 };
555 /**
556  * Displays a submission error within a overlay on the cell that failed update
557  *
558  * @function
559  * @this {M.gradereport_grader.classes.ajax}
560  * @param {String} message
561  * @param {Y.Node} cell
562  */
563 M.gradereport_grader.classes.ajax.prototype.display_submission_error = function(message, cell) {
564     var erroroverlay = new this.report.Y.Overlay({
565         headerContent : '<div><strong class="error">'+M.util.get_string('ajaxerror', 'gradereport_grader')+'</strong>  <em>'+M.util.get_string('ajaxclicktoclose', 'gradereport_grader')+'</em></div>',
566         bodyContent : message,
567         visible : false,
568         zIndex : 3
569     });
570     erroroverlay.set('xy', [cell.getX()+10,cell.getY()+10]);
571     erroroverlay.render(this.report.table.ancestor('div'));
572     erroroverlay.show();
573     erroroverlay.get('boundingBox').on('click', function(){
574         this.get('boundingBox').setStyle('visibility', 'hidden');
575         this.hide();
576         this.destroy();
577     }, erroroverlay);
578     erroroverlay.get('boundingBox').setStyle('visibility', 'visible');
579 };
580 /**
581  * A class for existing fields
582  * This class is used only when the user is in editing mode
583  *
584  * @class existingfield
585  * @constructor
586  * @param {M.gradereport_grader.classes.report} report
587  * @param {Int} userid
588  * @param {Int} itemid
589  */
590 M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
591     this.report = ajax.report;
592     this.userid = userid;
593     this.itemid = itemid;
594     this.editfeedback = ajax.showquickfeedback;
595     this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid);
597     if (this.grade !== null) {
598         for (var i = 0; i < this.report.grades.length; i++) {
599             if (this.report.grades[i]['user']==this.userid && this.report.grades[i]['item']==this.itemid) {
600                 this.oldgrade = this.report.grades[i]['grade'];
601             }
602         }
604         if (!this.oldgrade) {
605             // Assigning an empty string makes determining whether the grade has been changed easier
606             // This value is never sent to the server
607             this.oldgrade = '';
608         }
610         // On blur save any changes in the grade field
611         this.grade.on('blur', this.submit, this);
613     }
615     // Check if feedback is enabled
616     if (this.editfeedback) {
617         // Get the feedback fields
618         this.feedback = this.report.Y.one('#feedback_'+userid+'_'+itemid);
620         if (this.feedback !== null) {
621             for(var i = 0; i < this.report.feedback.length; i++) {
622                 if (this.report.feedback[i]['user']==this.userid && this.report.feedback[i]['item']==this.itemid) {
623                     this.oldfeedback = this.report.feedback[i]['content'];
624                 }
625             }
627             if(!this.oldfeedback) {
628                 // Assigning an empty string makes determining whether the feedback has been changed easier
629                 // This value is never sent to the server
630                 this.oldfeedback = '';
631             }
633             // On blur save any changes in the feedback field
634             this.feedback.on('blur', this.submit, this);
636             // Override the default tab movements when moving between cells
637             this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.feedback, 'press:9', this, true));                   // Handle Tab
638             this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.feedback, 'press:13', this));                // Handle the Enter key being pressed
639             this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.feedback, 'press:37,38,39,40+ctrl', this)); // Handle CTRL + arrow keys
642             if (this.grade !== null) {
643                 // Override the default tab movements for fields in the same cell
644                 this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();this.grade.focus();}, this.feedback, 'press:9+shift', this));
645                 this.keyevents.push(this.report.Y.on('key', function(e){if (e.shiftKey) {return;}e.preventDefault();this.feedback.focus();}, this.grade, 'press:9', this));
647                 // Override the default tab movements when moving between cells
648                 if (this.editfeedback) {
649                     this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9+shift', this));                // Handle Shift+Tab
650                 }
651             }
652         }
653     } else if (this.grade !== null) {
654         this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this));                      // Handle Tab and Shift+Tab
655     }
656     if (this.feedback !== null) {
657         this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'press:13', this));                   // Handle the Enter key being pressed
658         this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'press:37,38,39,40+ctrl', this));    // Handle CTRL + arrow keys
659     }
660 };
661 /**
662  * Attach the required properties and methods to the existing field class
663  * via prototyping
664  */
665 M.gradereport_grader.classes.existingfield.prototype.userid = null;
666 M.gradereport_grader.classes.existingfield.prototype.itemid = null;
667 M.gradereport_grader.classes.existingfield.prototype.editfeedback = false;
668 M.gradereport_grader.classes.existingfield.prototype.grade = null;
669 M.gradereport_grader.classes.existingfield.prototype.oldgrade = null;
670 M.gradereport_grader.classes.existingfield.prototype.keyevents = [];
671 /**
672  * Handles saving of changed on keypress
673  *
674  * @function
675  * @this {M.gradereport_grader.classes.existingfield}
676  * @param {Event} e
677  */
678 M.gradereport_grader.classes.existingfield.prototype.keypress_enter = function(e) {
679     e.preventDefault();
680     this.submit();
681 };
682 /**
683  * Handles setting the correct focus if the user presses tab
684  *
685  * @function
686  * @this {M.gradereport_grader.classes.existingfield}
687  * @param {Event} e
688  * @param {Bool} ignoreshift
689  */
690 M.gradereport_grader.classes.existingfield.prototype.keypress_tab = function(e, ignoreshift) {
691     e.preventDefault();
692     var next = null;
693     if (e.shiftKey) {
694         if (ignoreshift) {
695             return;
696         }
697         next = this.report.ajax.get_above_cell(this.grade.ancestor('td'));
698     } else {
699         next = this.report.ajax.get_below_cell(this.grade.ancestor('td'));
700     }
701     this.move_focus(next);
702 };
703 /**
704  * Handles setting the correct focus when the user presses CTRL+arrow keys
705  *
706  * @function
707  * @this {M.gradereport_grader.classes.existingfield}
708  * @param {Event} e
709  */
710 M.gradereport_grader.classes.existingfield.prototype.keypress_arrows = function(e) {
711     var next = null;
712     switch (e.keyCode) {
713         case 37:    // Left
714             next = this.report.ajax.get_prev_cell(this.grade.ancestor('td'));
715             break;
716         case 38:    // Up
717             next = this.report.ajax.get_above_cell(this.grade.ancestor('td'));
718             break;
719         case 39:    // Right
720             next = this.report.ajax.get_next_cell(this.grade.ancestor('td'));
721             break;
722         case 40:    // Down
723             next = this.report.ajax.get_below_cell(this.grade.ancestor('td'));
724             break;
725     }
726     this.move_focus(next);
727 };
728 /**
729  * Move the focus to the node
730  * @function
731  * @this {M.gradereport_grader.classes.existingfield}
732  * @param {Y.Node} node
733  */
734 M.gradereport_grader.classes.existingfield.prototype.move_focus = function(node) {
735     if (node) {
736         var properties = this.report.get_cell_info(node);
737         switch(properties.itemtype) {
738             case 'scale':
739                 properties.cell.one('select.select').focus();
740                 break;
741             case 'value':
742             default:
743                 properties.cell.one('input.text').focus();
744                 break;
745         }
746     }
747 };
748 /**
749  * Checks if the values for the field have changed
750  *
751  * @function
752  * @this {M.gradereport_grader.classes.existingfield}
753  * @return {Bool}
754  */
755 M.gradereport_grader.classes.existingfield.prototype.has_changed = function() {
756     if (this.editfeedback) {
757         return (this.grade.get('value') !== this.oldgrade || this.feedback.get('value') !== this.oldfeedback);
758     }
759     return (this.grade.get('value') !== this.oldgrade);
760 };
761 /**
762  * Submits any changes and then updates the fields accordingly
763  *
764  * @function
765  * @this {M.gradereport_grader.classes.existingfield}
766  */
767 M.gradereport_grader.classes.existingfield.prototype.submit = function() {
768     if (!this.has_changed()) {
769         return;
770     }
772     var properties = this.report.get_cell_info([this.userid,this.itemid]);
773     var values = (function(f){
774         var feedback, oldfeedback = null;
775         if (f.editfeedback) {
776             feedback = f.feedback.get('value');
777             oldfeedback = f.oldfeedback;
778         }
779         return {
780             editablefeedback : f.editfeedback,
781             grade : f.grade.get('value'),
782             oldgrade : f.oldgrade,
783             feedback : feedback,
784             oldfeedback : oldfeedback
785         };
786     })(this);
788     this.oldgrade = values.grade;
789     if (values.editablefeedback && values.feedback != values.oldfeedback) {
790         this.report.update_feedback(this.userid, this.itemid, values.feedback);
791         this.oldfeedback = values.feedback;
792     }
794     this.report.ajax.submit(properties, values);
795 };
797 /**
798  * Textfield class
799  * This classes gets used in conjunction with the report running with AJAX enabled
800  * and is used to manage a cell that has a grade requiring a textfield for input
801  *
802  * @class textfield
803  * @constructor
804  * @this {M.gradereport_grader.classes.textfield}
805  * @param {M.gradereport_grader.classes.report} report
806  * @param {Y.Node} node
807  */
808 M.gradereport_grader.classes.textfield = function(report, node) {
809     this.report = report;
810     this.node = node;
811     this.gradespan = node.one('.gradevalue');
812     this.inputdiv = this.report.Y.Node.create('<div></div>');
813     this.editfeedback = this.report.ajax.showquickfeedback;
814     this.grade = this.report.Y.Node.create('<input type="text" class="text" value="" />');
815     this.gradetype = 'value';
816     this.inputdiv.append(this.grade);
817     if (this.report.ajax.showquickfeedback) {
818         this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" />');
819         this.inputdiv.append(this.feedback);
820     }
821 };
822 /**
823  * Extend the textfield class with the following methods and properties
824  */
825 M.gradereport_grader.classes.textfield.prototype.keyevents = [];
826 M.gradereport_grader.classes.textfield.prototype.editable = false;
827 M.gradereport_grader.classes.textfield.prototype.gradetype = null;
828 M.gradereport_grader.classes.textfield.prototype.grade = null;
829 M.gradereport_grader.classes.textfield.prototype.report = null;
830 M.gradereport_grader.classes.textfield.prototype.node = null;
831 M.gradereport_grader.classes.textfield.prototype.gradespam = null;
832 M.gradereport_grader.classes.textfield.prototype.inputdiv = null;
833 M.gradereport_grader.classes.textfield.prototype.editfeedback = false;
834 /**
835  * Replaces the cell contents with the controls to enable editing
836  *
837  * @function
838  * @this {M.gradereport_grader.classes.textfield}
839  * @return {M.gradereport_grader.classes.textfield}
840  */
841 M.gradereport_grader.classes.textfield.prototype.replace = function() {
842     this.set_grade(this.get_grade());
843     if (this.editfeedback) {
844         this.set_feedback(this.get_feedback());
845     }
846     this.node.replaceChild(this.inputdiv, this.gradespan);
847     this.grade.focus();
848     this.editable = true;
849     return this;
850 };
851 /**
852  * Commits the changes within a cell and returns a result object of new + old values
853  * @function
854  * @this {M.gradereport_grader.classes.textfield}
855  * @return {Object}
856  */
857 M.gradereport_grader.classes.textfield.prototype.commit = function() {
858     // Produce an anonymous result object contianing all values
859     var result = (function(field){
860         field.editable = false;
861         var oldgrade = field.get_grade();
862         if (oldgrade == '-') {
863             oldgrade = '';
864         }
865         var feedback = null;
866         var oldfeedback = null;
867         if (field.editfeedback) {
868             oldfeedback = field.get_feedback();
869         }
870         field.editable = true;
871         if (field.editfeedback) {
872             feedback = field.get_feedback();
873         }
874         return {
875             gradetype : field.gradetype,
876             editablefeedback : field.editfeedback,
877             grade : field.get_grade(),
878             oldgrade : oldgrade,
879             feedback : feedback,
880             oldfeedback : oldfeedback
881         };
882     })(this);
883     // Set the changes in stone
884     this.set_grade(result.grade);
885     if (this.editfeedback) {
886         this.set_feedback(result.feedback);
887     }
888     // Return the result object
889     return result;
890 };
891 /**
892  * Reverts a cell back to its static contents
893  * @function
894  * @this {M.gradereport_grader.classes.textfield}
895  */
896 M.gradereport_grader.classes.textfield.prototype.revert = function() {
897     this.node.replaceChild(this.gradespan, this.inputdiv);
898     for (var i in this.keyevents) {
899         if (this.keyevents[i]) {
900             this.keyevents[i].detach();
901         }
902     }
903     this.keyevents = [];
904     this.node.on('click', this.report.ajax.make_editable, this.report.ajax);
905 };
906 /**
907  * Gets the grade for current cell
908  *
909  * @function
910  * @this {M.gradereport_grader.classes.textfield}
911  * @return {Mixed}
912  */
913 M.gradereport_grader.classes.textfield.prototype.get_grade = function() {
914     if (this.editable) {
915         return this.grade.get('value');
916     }
917     return this.gradespan.get('innerHTML');
918 };
919 /**
920  * Sets the grade for the current cell
921  * @function
922  * @this {M.gradereport_grader.classes.textfield}
923  * @param {Mixed} value
924  */
925 M.gradereport_grader.classes.textfield.prototype.set_grade = function(value) {
926     if (!this.editable) {
927         if (value == '-') {
928             value = '';
929         }
930         this.grade.set('value', value);
931     } else {
932         if (value == '') {
933             value = '-';
934         }
935         this.gradespan.set('innerHTML', value);
936     }
937 };
938 /**
939  * Gets the feedback for the current cell
940  * @function
941  * @this {M.gradereport_grader.classes.textfield}
942  * @return {String}
943  */
944 M.gradereport_grader.classes.textfield.prototype.get_feedback = function() {
945     if (this.editable) {
946         return this.feedback.get('value');
947     }
948     var properties = this.report.get_cell_info(this.node);
949     if (properties) {
950         return properties.feedback;
951     }
952     return '';
953 };
954 /**
955  * Sets the feedback for the current cell
956  * @function
957  * @this {M.gradereport_grader.classes.textfield}
958  * @param {Mixed} value
959  */
960 M.gradereport_grader.classes.textfield.prototype.set_feedback = function(value) {
961     if (!this.editable) {
962         this.feedback.set('value', value);
963     } else {
964         var properties = this.report.get_cell_info(this.node);
965         this.report.update_feedback(properties.userid, properties.itemid, value);
966     }
967 };
968 /**
969  * Checks if the current cell has changed at all
970  * @function
971  * @this {M.gradereport_grader.classes.textfield}
972  * @return {Bool}
973  */
974 M.gradereport_grader.classes.textfield.prototype.has_changed = function() {
975     // If its not editable it has not changed
976     if (!this.editable) {
977         return false;
978     }
979     // If feedback is being edited then it has changed if either grade or feedback have changed
980     if (this.editfeedback) {
981         var properties = this.report.get_cell_info(this.node);
982         if (this.get_feedback() != properties.feedback) {
983             return true;
984         }
985     }
986     return (this.get_grade() != this.gradespan.get('innerHTML'));
987 };
988 /**
989  * Attaches the key listeners for the editable fields and stored the event references
990  * against the textfield
991  *
992  * @function
993  * @this {M.gradereport_grader.classes.textfield}
994  */
995 M.gradereport_grader.classes.textfield.prototype.attach_key_events = function() {
996     var a = this.report.ajax;
997     // Setup the default key events for tab and enter
998     if (this.editfeedback) {
999         this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9+shift', a));               // Handle Shift+Tab
1000         this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'press:9', a, true));            // Handle Tab
1001         this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'press:13', a));               // Handle the Enter key being pressed
1002     } else {
1003         this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9', a));                     // Handle Tab and Shift+Tab
1004     }
1005     this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'press:13', a));                      // Handle the Enter key being pressed
1006     // Setup the arrow key events
1007     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
1008     // Prevent the default key action on all fields for arrow keys on all key events!
1009     // Note: this still does not work in FF!!!!!
1010     this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'down:37,38,39,40+ctrl'));
1011     this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'press:37,38,39,40+ctrl'));
1012     this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'up:37,38,39,40+ctrl'));
1013 };
1015 /**
1016  * An editable scale field
1017  *
1018  * @class scalefield
1019  * @constructor
1020  * @inherits M.gradereport_grader.classes.textfield
1021  * @base M.gradereport_grader.classes.textfield
1022  * @this {M.gradereport_grader.classes.scalefield}
1023  * @param {M.gradereport_grader.classes.report} report
1024  * @param {Y.Node} node
1025  */
1026 M.gradereport_grader.classes.scalefield = function(report, node) {
1027     this.report = report;
1028     this.node = node;
1029     this.gradespan = node.one('.gradevalue');
1030     this.inputdiv = this.report.Y.Node.create('<div></div>');
1031     this.editfeedback = this.report.ajax.showquickfeedback;
1032     this.grade = this.report.Y.Node.create('<select type="text" class="text" /><option value="-1">'+M.util.get_string('ajaxchoosescale', 'gradereport_grader')+'</option></select>');
1033     this.gradetype = 'scale';
1034     this.inputdiv.append(this.grade);
1035     if (this.editfeedback) {
1036         this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" />');
1037         this.inputdiv.append(this.feedback);
1038     }
1039     var properties = this.report.get_cell_info(node);
1040     this.scale = this.report.ajax.scales[properties.itemscale];
1041     for (var i in this.scale) {
1042         if (this.scale[i]) {
1043             this.grade.append(this.report.Y.Node.create('<option value="'+(parseFloat(i)+1)+'">'+this.scale[i]+'</option>'));
1044         }
1045     }
1046 };
1047 /**
1048  * Override + extend the scalefield class with the following properties
1049  * and methods
1050  */
1051 /**
1052  * @property {Array} scale
1053  */
1054 M.gradereport_grader.classes.scalefield.prototype.scale = [];
1055 /**
1056  * Extend the scalefield with the functions from the textfield
1057  */
1058 /**
1059  * Overrides the get_grade function so that it can pick up the value from the
1060  * scales select box
1061  *
1062  * @function
1063  * @this {M.gradereport_grader.classes.scalefield}
1064  * @return {Int} the scale id
1065  */
1066 M.gradereport_grader.classes.scalefield.prototype.get_grade = function(){
1067     if (this.editable) {
1068         // Return the scale value
1069         return this.grade.all('option').item(this.grade.get('selectedIndex')).get('value');
1070     } else {
1071         // Return the scale values id
1072         var value = this.gradespan.get('innerHTML');
1073         for (var i in this.scale) {
1074             if (this.scale[i] == value) {
1075                 return parseFloat(i)+1;
1076             }
1077         }
1078         return -1;
1079     }
1080 };
1081 /**
1082  * Overrides the set_grade function of textfield so that it can set the scale
1083  * within the scale select box
1084  *
1085  * @function
1086  * @this {M.gradereport_grader.classes.scalefield}
1087  * @param {String} value
1088  */
1089 M.gradereport_grader.classes.scalefield.prototype.set_grade = function(value) {
1090     if (!this.editable) {
1091         if (value == '-') {
1092             value = '-1';
1093         }
1094         this.grade.all('option').each(function(node){
1095             if (node.get('value') == value) {
1096                 node.set('selected', true);
1097             }
1098         });
1099     } else {
1100         if (value == '' || value == '-1') {
1101             value = '-';
1102         } else {
1103             value = this.scale[parseFloat(value)-1];
1104         }
1105         this.gradespan.set('innerHTML', value);
1106     }
1107 };
1108 /**
1109  * Checks if the current cell has changed at all
1110  * @function
1111  * @this {M.gradereport_grader.classes.scalefield}
1112  * @return {Bool}
1113  */
1114 M.gradereport_grader.classes.scalefield.prototype.has_changed = function() {
1115     if (!this.editable) {
1116         return false;
1117     }
1118     var gradef = this.get_grade();
1119     this.editable = false;
1120     var gradec = this.get_grade();
1121     this.editable = true;
1122     if (this.editfeedback) {
1123         var properties = this.report.get_cell_info(this.node);
1124         var feedback = properties.feedback;
1125         return (gradef != gradec || this.get_feedback() != feedback);
1126     }
1127     return (gradef != gradec);
1128 };
1130 /**
1131  * Manually extend the scalefield class with the properties and methods of the
1132  * textfield class that have not been defined
1133  */
1134 for (var i in M.gradereport_grader.classes.textfield.prototype) {
1135     if (!M.gradereport_grader.classes.scalefield.prototype[i]) {
1136         M.gradereport_grader.classes.scalefield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i];
1137     }