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