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