MDL-36606 gradereport: Correctly handle ungraded assessment ajax grading
[moodle.git] / grade / report / grader / module.js
CommitLineData
fe213365
SH
1/**
2 * Grader report namespace
3 */
4M.gradereport_grader = {
fe213365
SH
5 /**
6 * @namespace M.gradereport_grader
7 * @param {Object} reports A collection of classes used by the grader report module
8 */
9 classes : {},
fe213365
SH
10 /**
11 * Instantiates a new grader report
12 *
13 * @function
14 * @param {YUI} Y
6ef4878b 15 * @param {Object} cfg A configuration object
fe213365
SH
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
a740a0ad 19 * @param {Array} An array of student grades
fe213365 20 */
eede44b5 21 init_report : function(Y, cfg, items, users, feedback, grades) {
fe213365 22 // Create the actual report
eede44b5 23 new this.classes.report(Y, cfg, items, users, feedback, grades);
fe213365
SH
24 }
25};
26
27/**
28 * Initialises the JavaScript for the gradebook grader report
29 *
a740a0ad
AD
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 *
fe213365
SH
35 * @class report
36 * @constructor
37 * @this {M.gradereport_grader}
38 * @param {YUI} Y
fe213365
SH
39 * @param {Object} cfg Configuration variables
40 * @param {Array} items An array containing grade items
6ef4878b 41 * @param {Array} users An array containing user information
fe213365
SH
42 * @param {Array} feedback An array containing feedback information
43 */
eede44b5 44M.gradereport_grader.classes.report = function(Y, cfg, items, users, feedback, grades) {
fe213365
SH
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');
a740a0ad 52 this.grades = grades;
fe213365 53
fe213365
SH
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 */
62M.gradereport_grader.classes.report.prototype.table = null; // YUI Node for the reports main table
63M.gradereport_grader.classes.report.prototype.items = []; // Array containing grade items
64M.gradereport_grader.classes.report.prototype.users = []; // Array containing user information
65M.gradereport_grader.classes.report.prototype.feedback = []; // Array containing feedback items
66M.gradereport_grader.classes.report.prototype.ajaxenabled = false; // True is AJAX is enabled for the report
67M.gradereport_grader.classes.report.prototype.ajax = null; // An instance of the ajax class or null
fe213365
SH
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 */
78M.gradereport_grader.classes.report.prototype.get_cell_info = function(arg) {
79
80 var userid= null;
81 var itemid = null;
a740a0ad 82 var feedback = ''; // Don't default feedback to null or string comparisons become error prone
fe213365
SH
83 var cell = null;
84 var i = null;
85
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 }
100
101 if (!cell) {
102 return null;
103 }
104
1af4cced
MM
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 }
111
fe213365 112 return {
19db454f 113 id : cell.getAttribute('id'),
fe213365
SH
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,
1af4cced 121 feedback : feedback,
fe213365
SH
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 */
135M.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};
fe213365 145/**
6ef4878b 146 * Initialises the AJAX component of this report
fe213365
SH
147 * @class ajax
148 * @constructor
149 * @this {M.gradereport_grader.ajax}
150 * @param {M.gradereport_grader.classes.report} report
151 * @param {Object} cfg
152 */
153M.gradereport_grader.classes.ajax = function(report, cfg) {
154 this.report = report;
155 this.courseid = cfg.courseid || null;
bab41ab7 156 this.feedbacktrunclength = cfg.feedbacktrunclength || null;
fe213365
SH
157 this.studentsperpage = cfg.studentsperpage || null;
158 this.showquickfeedback = cfg.showquickfeedback || false;
159 this.scales = cfg.scales || null;
160 this.existingfields = [];
161
a740a0ad 162 if (!report.isediting) {
0e999796 163 report.table.all('.clickable').on('click', this.make_editable, this);
fe213365
SH
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) {
a740a0ad 170 this.existingfields[userid][itemid] = new M.gradereport_grader.classes.existingfield(this, userid, itemid);
fe213365
SH
171 }
172 }
60daf98c 173 // Disable the Update button as we're saving using ajax.
a740a0ad 174 submitbutton = this.report.Y.one('#gradersubmit');
60daf98c 175 submitbutton.set('disabled', true);
fe213365
SH
176 }
177};
178/**
179 * Extend the ajax class with the following methods and properties
180 */
181M.gradereport_grader.classes.ajax.prototype.report = null; // A reference to the report class this object will use
182M.gradereport_grader.classes.ajax.prototype.courseid = null; // The id for the course being viewed
183M.gradereport_grader.classes.ajax.prototype.feedbacktrunclength = null; // The length to truncate feedback to
184M.gradereport_grader.classes.ajax.prototype.studentsperpage = null; // The number of students shown per page
185M.gradereport_grader.classes.ajax.prototype.showquickfeedback = null; // True if feedback editing should be shown
186M.gradereport_grader.classes.ajax.prototype.current = null; // The field being currently editing
187M.gradereport_grader.classes.ajax.prototype.pendingsubmissions = []; // Array containing pending IO transactions
188M.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 */
194M.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);
204
205 if (this.current) {
206 // Current is already set!
207 this.process_editable_field(node);
208 return;
209 }
210
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();
325e81d3
AN
227
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');
fe213365
SH
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 */
238M.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 */
249M.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 */
267M.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 */
293M.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 }
325e81d3
AN
306
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');
fe213365
SH
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 */
317M.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 */
336M.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 */
356M.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 */
380M.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 }
a740a0ad 392 // next will be null when we get to the bottom of a column
fe213365
SH
393 return next;
394};
395/**
396 * Submits changes for update
397 *
398 * @function
399 * @this {M.gradereport_grader.classes.ajax}
6ef4878b 400 * @param {Object} properties Properties of the cell being edited
fe213365
SH
401 * @param {Object} values Object containing old + new values
402 */
403M.gradereport_grader.classes.ajax.prototype.submit = function(properties, values) {
fe213365
SH
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 */
452M.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) {
64e7aa4d 457 var message = M.util.get_string('ajaxfailedupdate', 'gradereport_grader');
b55d37f0
AN
458 message = message.replace(/\[1\]/, args.type);
459 message = message.replace(/\[2\]/, this.report.users[args.properties.userid]);
a740a0ad 460
fe213365
SH
461 this.display_submission_error(message, args.properties.cell);
462 return;
463 }
464
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];
6ef4878b 474 // Get the cell referred to by this result object
fe213365
SH
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 = '';
a740a0ad
AD
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 }
fe213365 489 } else {
a740a0ad
AD
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 }
fe213365
SH
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!
64e7aa4d 517 if (!this.current.has_changed() || confirm(M.util.get_string('ajaxfieldchanged', 'gradereport_grader'))) {
fe213365
SH
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 */
563M.gradereport_grader.classes.ajax.prototype.display_submission_error = function(message, cell) {
564 var erroroverlay = new this.report.Y.Overlay({
64e7aa4d 565 headerContent : '<div><strong class="error">'+M.util.get_string('ajaxerror', 'gradereport_grader')+'</strong> <em>'+M.util.get_string('ajaxclicktoclose', 'gradereport_grader')+'</em></div>',
fe213365
SH
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 */
a740a0ad
AD
590M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
591 this.report = ajax.report;
fe213365
SH
592 this.userid = userid;
593 this.itemid = itemid;
a740a0ad
AD
594 this.editfeedback = ajax.showquickfeedback;
595 this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid);
596
df65120a 597 if (this.grade !== null) {
ec7298a1
AD
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 }
a740a0ad 602 }
a740a0ad 603
df65120a
ARN
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 }
fe213365 609
df65120a
ARN
610 // On blur save any changes in the grade field
611 this.grade.on('blur', this.submit, this);
612
613 }
fe213365
SH
614
615 // Check if feedback is enabled
616 if (this.editfeedback) {
617 // Get the feedback fields
a740a0ad
AD
618 this.feedback = this.report.Y.one('#feedback_'+userid+'_'+itemid);
619
df65120a
ARN
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 }
a740a0ad 625 }
a740a0ad 626
df65120a
ARN
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 }
a740a0ad 632
df65120a
ARN
633 // On blur save any changes in the feedback field
634 this.feedback.on('blur', this.submit, this);
fe213365 635
df65120a
ARN
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
fe213365 640
df65120a
ARN
641
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));
646
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) {
a740a0ad 654 this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this)); // Handle Tab and Shift+Tab
fe213365 655 }
df65120a
ARN
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 }
fe213365
SH
660};
661/**
662 * Attach the required properties and methods to the existing field class
663 * via prototyping
664 */
665M.gradereport_grader.classes.existingfield.prototype.userid = null;
666M.gradereport_grader.classes.existingfield.prototype.itemid = null;
667M.gradereport_grader.classes.existingfield.prototype.editfeedback = false;
668M.gradereport_grader.classes.existingfield.prototype.grade = null;
669M.gradereport_grader.classes.existingfield.prototype.oldgrade = null;
670M.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 */
678M.gradereport_grader.classes.existingfield.prototype.keypress_enter = function(e) {
a740a0ad 679 e.preventDefault();
fe213365
SH
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 */
690M.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 */
710M.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 */
734M.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 */
755M.gradereport_grader.classes.existingfield.prototype.has_changed = function() {
756 if (this.editfeedback) {
a740a0ad 757 return (this.grade.get('value') !== this.oldgrade || this.feedback.get('value') !== this.oldfeedback);
fe213365 758 }
a740a0ad 759 return (this.grade.get('value') !== this.oldgrade);
fe213365
SH
760};
761/**
762 * Submits any changes and then updates the fields accordingly
763 *
764 * @function
765 * @this {M.gradereport_grader.classes.existingfield}
766 */
767M.gradereport_grader.classes.existingfield.prototype.submit = function() {
768 if (!this.has_changed()) {
769 return;
770 }
a740a0ad 771
fe213365
SH
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');
a740a0ad 777 oldfeedback = f.oldfeedback;
fe213365
SH
778 }
779 return {
780 editablefeedback : f.editfeedback,
781 grade : f.grade.get('value'),
a740a0ad 782 oldgrade : f.oldgrade,
fe213365
SH
783 feedback : feedback,
784 oldfeedback : oldfeedback
785 };
786 })(this);
a740a0ad
AD
787
788 this.oldgrade = values.grade;
fe213365
SH
789 if (values.editablefeedback && values.feedback != values.oldfeedback) {
790 this.report.update_feedback(this.userid, this.itemid, values.feedback);
a740a0ad 791 this.oldfeedback = values.feedback;
fe213365 792 }
a740a0ad 793
fe213365
SH
794 this.report.ajax.submit(properties, values);
795};
796
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 */
808M.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 */
825M.gradereport_grader.classes.textfield.prototype.keyevents = [];
826M.gradereport_grader.classes.textfield.prototype.editable = false;
827M.gradereport_grader.classes.textfield.prototype.gradetype = null;
828M.gradereport_grader.classes.textfield.prototype.grade = null;
829M.gradereport_grader.classes.textfield.prototype.report = null;
830M.gradereport_grader.classes.textfield.prototype.node = null;
831M.gradereport_grader.classes.textfield.prototype.gradespam = null;
832M.gradereport_grader.classes.textfield.prototype.inputdiv = null;
833M.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 */
841M.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 */
857M.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 */
896M.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 = [];
0e999796 904 this.node.on('click', this.report.ajax.make_editable, this.report.ajax);
fe213365
SH
905};
906/**
907 * Gets the grade for current cell
908 *
909 * @function
910 * @this {M.gradereport_grader.classes.textfield}
911 * @return {Mixed}
912 */
913M.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 */
925M.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 */
944M.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) {
1af4cced 950 return properties.feedback;
fe213365
SH
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 */
960M.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 */
974M.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);
a740a0ad
AD
982 if (this.get_feedback() != properties.feedback) {
983 return true;
984 }
fe213365
SH
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 */
995M.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};
1014
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 */
1026M.gradereport_grader.classes.scalefield = function(report, node) {
46b7d289 1027 this.report = report;
fe213365
SH
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;
64e7aa4d 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>');
fe213365
SH
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/**
6ef4878b 1048 * Override + extend the scalefield class with the following properties
fe213365
SH
1049 * and methods
1050 */
1051/**
1052 * @property {Array} scale
1053 */
1054M.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 */
1066M.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 */
1089M.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 */
1114M.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);
1af4cced 1124 var feedback = properties.feedback;
fe213365
SH
1125 return (gradef != gradec || this.get_feedback() != feedback);
1126 }
1127 return (gradef != gradec);
1128};
1129
1130/**
1131 * Manually extend the scalefield class with the properties and methods of the
1132 * textfield class that have not been defined
1133 */
1134for (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 }
8233747e 1138}