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