[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/grade/report/grader/ -> module.js (source)

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1