/**
  *  Gradebook data grid
  *
  *  PORTIONS OF THIS FILE ARE BASED ON RICO LIVEGRID 1.1.2
  *
  *  Copyright 2005 Sabre Airline Solutions
  *
  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
  *  file except in compliance with the License. You may obtain a copy of the License at
  *
  *         http://www.apache.org/licenses/LICENSE-2.0
  *
  *  Unless required by applicable law or agreed to in writing, software distributed under the
  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
  *  either express or implied. See the License for the specific language governing permissions
  *  and limitations under the License.
  *
  *  @author "Bill Richard"
  *  @version  
  *
  *
  **/

Gradebook.ColDef = Class.create();
Gradebook.ColDef.prototype = {
  initialize: function(jsonObj, model, schemaMap) {
    this.model = model;
    Object.extend(this, jsonObj); // assign json properties to this object
    if (this.sid){
      this.primarySchema = schemaMap[this.sid];
    }
    if (this.ssid){
      this.secondarySchema = schemaMap[this.ssid];
    }
  },

  getSortFunction: function(sortColumn, sortdir, secondarySortColumn) {
    this.sortColumn = sortColumn;
    this.secondarySortColumn = secondarySortColumn;
    if (sortdir=='ASC')
      return this._sortASC.bind(this);
    else
      return this._sortDESC.bind(this);
  },

  validate: function(newValue, matchPartial) {
    if (!this.primarySchema){
      return null;
    } else {
      return this.primarySchema.validate(newValue, matchPartial);
    }
  },

  _sortASC: function(a,b) {
    var aa = a[this.sortColumn].v;
    var bb = b[this.sortColumn].v;
        if( !aa && !bb ) return this._secondarySortASC(a,b);
        if( !aa ) return -1;
        if( !bb ) return 1;
        aa = aa.toUpperCase();
        bb = bb.toUpperCase();
    if (aa==bb)return this._secondarySortASC(a,b);
    if (aa<bb) return -1;
    return 1;
  },

  _secondarySortASC: function(a,b) {
    var aa = a[this.secondarySortColumn].v;
    var bb = b[this.secondarySortColumn].v;
        if( !aa || !bb ) return 0;
        aa = aa.toUpperCase();
        bb = bb.toUpperCase();
      if (aa==bb) return 0;
    if (aa<bb) return -1;
    return 1;
  },

  _sortDESC: function(a,b) {
    var aa = a[this.sortColumn].v;
    var bb = b[this.sortColumn].v;
        if( !aa && !bb ) return this._secondarySortDESC(a,b);
        if( !bb ) return -1;
        if( !aa ) return 1;
        aa = aa.toUpperCase();
        bb = bb.toUpperCase();
    if (aa==bb)return this._secondarySortDESC(a,b);
    if (bb<aa) return -1;
    return 1;
  },

  _secondarySortDESC: function(a,b) {
    var aa = a[this.secondarySortColumn].v;
    var bb = b[this.secondarySortColumn].v;
        if( !aa || !bb ) return 0;
        aa = aa.toUpperCase();
        bb = bb.toUpperCase();
      if (aa==bb) return 0;
    if (bb<aa) return -1;
    return 1;
  },

  getEditValue: function( gridCell ) {
    if (!this.primarySchema){
      return gridCell.getValue();
    }
    return this.primarySchema.getEditValue( gridCell );
  },


  // called by GridCell.getCellValue to get value for rendering in spreadsheet
  // uses primary (and optional secondary) schema to convert value to proper display format
  getCellValue: function( gridCell ){
    if (!this.primarySchema){
      return gridCell.getValue();
    }
    var cellVal = this.primarySchema.getCellValue( gridCell );
    if (this.secondarySchema){
      var cellVal2 = this.secondarySchema.getCellValue( gridCell );
      cellVal += ' <span>('+cellVal2+')</span>';
    }
          
    return new String(cellVal);
  },

  // called by GridCell.getAltValue to get alt (mouse over) value for rendering in spreadsheet
  // same as getCellValue unless there is a secondary schema
  getAltValue: function( gridCell ){
    if (gridCell.isExempt()) {
      return this.model.getMessage('cmExemptGrade');
    }
    
    if (!this.secondarySchema){
      return this.getCellValue( gridCell );
    }
    var cellVal = this.primarySchema.getCellValue( gridCell );
    if (this.secondarySchema){
      var cellVal2 = this.secondarySchema.getCellValue( gridCell );
      cellVal += ' ('+cellVal2+')';
    }
    return new String(cellVal);
  },

  getSortValue: function( gridCell ){
    return gridCell.getValue();
  },

  getName: function() {
    return this.name;
  },

  getID: function() {
    return this.id;
  },

  getPoints: function() {
    return this.points;
  },

  getPointsForDisplay: function() {
    var formattedPoints = NumberFormatter.getDisplayFloat( this.points );
    if ( this.isCalculated() )
    {
      var msgTemplate = new Template( GradebookUtil.getMessage( 'variesPerStudentMsg' ) );
      return msgTemplate.evaluate( { points:formattedPoints } );
    }
    return formattedPoints;
  },  
  
  getAliasID: function() {
    return this.id;
  },

  getCategoryID: function() {
    return this.catid;
  },

  getCategory: function() {
      if (! this.catid ) return "";
      if (! this.model.catNameMap ) return "";
      var name = this.model.catNameMap[ Number(this.catid) ];
      if ( name ) return name;
    return "";
  },

  getCategoryAliasID: function() {
    return this.catid;
  },

  isHidden: function() {
    return !this.gbvis;
  },

  isScorable: function() {
    return this.scrble;
  },

  isPublic: function() {
    return (this.id == this.model.pubColID);
  },

  isVisibleToStudents: function() {
    return this.vis;
  },

  onHideColumn: function( evt ) {
    Event.stop( evt );
    this.gbvis = false;
    this.model.hideColumn(this.id);
  },

   onSortAscending: function ( evt ) {
    Event.stop( evt );
    this.cellController.onSortAscending();
   },
   
   onSortDescending: function ( evt ) {
    Event.stop( evt );
    this.cellController.onSortDescending();
   },
   
  onToggleColumnStudentVisibility : function( evt ){
    Event.stop( evt );
      this.model.setColumnStudentVisibility( this.id, !this.vis );
      this.cellController.closePopupsAndRestoreFocus(evt);
  },

  getDisplayType: function( ) {
    return this.primarySchema.type;
  },

  hasError: function( ) {
    return this.comput_err;
  },
  
  // called by model.getDisplayValue when external pages need to convert a rawValue
  // This function passes this.points to schema.getDisplayValue. 
  // This method should not be called for this colDef if this colDef is a calculated 
  // column, because we do not have access to the gridCell to get its max points.
  // todo: determine how to handle error condition if this column is a calulated col
  getDisplayValue: function( rawValue ) {
    if (this.primarySchema){
      return this.primarySchema.getDisplayValue( rawValue, this.points );
    }
    return rawValue;
  },
  
  getSecondaryDisplayValue: function( rawValue ) {
    if (this.secondarySchema){
      return this.secondarySchema.getDisplayValue( rawValue, this.points );
    } 
    return;
  }

};

Gradebook.GradeColDef = Class.create();
Object.extend(Gradebook.GradeColDef.prototype, Gradebook.ColDef.prototype);
Object.extend (Gradebook.GradeColDef.prototype, {     
  initialize: function(jsonObj, model, schemaMap) {
      this.linkrefid="";
    Gradebook.ColDef.prototype.initialize.call(this,jsonObj, model, schemaMap);
  },

  getRawValue: function( newValue ){
    var score = newValue;
    // compute score based on primary schema
    if (this.primarySchema){
      var rawValue = this.primarySchema.getRawValue(newValue,this);
      score = parseFloat( rawValue );
      if (!GradebookUtil.isValidFloat( rawValue )){
            if (typeof(rawValue) == "string")
              return rawValue;
        score = 0;
      }
    }
    return score;
  },

  getSortValue: function( gridCell ){
    if (this.primarySchema){
      return this.primarySchema.getSortValue( gridCell );
    } else {
      return gridCell.getValue();
    }
  },

  updateGrade: function( newValue, userId ){
    var score = this.getRawValue(newValue);
    var textValue = newValue;
    this.model.updateGrade(score, textValue, userId, this.id);
  },

  // get the grade for this column in the given row, use shared instance of gridcell A  
  // use for sort comparisons only... does not support multiple simultaneous instances 
  _getGradeA: function( row ) {
    if (this.colIndex == undefined) this.colIndex = this.model.colDefMap[this.id];
    var data = row[this.colIndex];
    if (data.metaData == undefined) data.metaData = row[0];
    if (data.colDef == undefined) data.colDef = this;
    var gc = Gradebook.GradeColDef.gridCellA;
    if (gc == undefined) {
      Gradebook.GradeColDef.gridCellA = new Gradebook.GridCell();
      gc = Gradebook.GradeColDef.gridCellA;
    }
	gc.setData(data);
    return gc;
  },

  // get the grade for this column in the given row, use shared instance of gridcell B  
  // use for sort comparisons only... does not support multiple simultaneous instances 
  _getGradeB: function( row ) {
    if (this.colIndex == undefined) this.colIndex = this.model.colDefMap[this.id];
    var data = row[this.colIndex];
    if (data.metaData == undefined) data.metaData = row[0];
    if (data.colDef == undefined) data.colDef = this;
    var gc = Gradebook.GradeColDef.gridCellB;
    if (gc == undefined) {
      Gradebook.GradeColDef.gridCellB = new Gradebook.GridCell();
      gc = Gradebook.GradeColDef.gridCellB;
    }
	gc.setData(data);
    return gc;
  },

  _sortASC: function(a,b) {
    var gradeA = this._getGradeA(a);
    var gradeB = this._getGradeB(b);
    var aa = gradeA.getSortValue();
    var bb = gradeB.getSortValue();
    if (gradeA.colDef.primarySchema instanceof Gradebook.TextSchema){
      if (aa==bb) return this._secondarySortASC(a,b);
      if (aa<bb) return -1;
      return 1;
    }
    var aaa = parseFloat(aa);
    var bbb = parseFloat(bb);
    var aNull = (aa == '-');
    var bNull = (bb == '-');
    var ax = gradeA.isExempt();
    var bx = gradeB.isExempt();
    var aIP = gradeA.attemptInProgress();
    var bIP = gradeB.attemptInProgress();
    var aNG = gradeA.needsGrading();
    var bNG = gradeB.needsGrading();
    var aNoScore = (aNull || ax || aIP || aNG || isNaN(aaa));
    var bNoScore = (bNull || bx || bIP || bNG || isNaN(bbb));
    var aVal = (ax)?1:(aIP)?2:(aNG)?3:(aNull)?0:aa;
    var bVal = (bx)?1:(bIP)?2:(bNG)?3:(bNull)?0:bb;
    if (aNoScore || bNoScore){
      if (aNoScore && bNoScore){
        if (aVal == bVal) return this._secondarySortASC(a,b);
        else return aVal-bVal;
      }
      if (aNoScore) return -1;
      else return 1;
    } else {
      if (aaa == bbb) return this._secondarySortASC(a,b);
      else return aaa-bbb;
    }
  },

  _sortDESC: function(a,b) {
    var gradeA = this._getGradeA(a);
    var gradeB = this._getGradeB(b);
    var aa = gradeA.getSortValue();
    var bb = gradeB.getSortValue();
    if (gradeA.colDef.primarySchema instanceof Gradebook.TextSchema){
      if (aa==bb) return this._secondarySortDESC(a,b);
      if (bb<aa) return -1;
      return 1;
    }
    var aaa = parseFloat(aa);
    var bbb = parseFloat(bb);
    var aNull = (aa == '-');
    var bNull = (bb == '-');
    var ax = gradeA.isExempt();
    var bx = gradeB.isExempt();
    var aIP = gradeA.attemptInProgress();
    var bIP = gradeB.attemptInProgress();
    var aNG = gradeA.needsGrading();
    var bNG = gradeB.needsGrading();
    var aNoScore = (aNull || ax || aIP || aNG || isNaN(aaa));
    var bNoScore = (bNull || bx || bIP || bNG || isNaN(bbb));
    var aVal = (ax)?1:(aIP)?2:(aNG)?3:(aNull)?0:aa;
    var bVal = (bx)?1:(bIP)?2:(bNG)?3:(bNull)?0:bb;
    if (aNoScore || bNoScore){
      if (aNoScore && bNoScore){
        if (aVal == bVal) return this._secondarySortDESC(a,b);
        else return bVal-aVal;
      }
      if (bNoScore) return -1;
      else return 1;
    } else {
      if (aaa == bbb) return this._secondarySortDESC(a,b);
      else return bbb-aaa;
    }
  },

  isGrade: function() {
    return true;
  },
  
  isCalculated: function() {
    return this.type != "N";
  },
  
  isTotal: function() {
    return this.type == "T";
  },
  
  isWeighted: function() {
    return this.type == "W";
  },
  
  getType: function() {
    switch (this.type){
     case "T": return 'total';
     case "W": return 'weighted';
     case "A": return 'average';
     case "M": return 'minMax';
    }
    return "grade";
  },
  
  isManual: function() {
    return this.manual;
  },
  
  isUserCreated: function() {
    return this.userCreated;
  },
  
  isAlignable: function() {
    return this.align && this.align == 'y';
  },
  
  isHideAttemptScore: function() {
    return this.hideAtt;
  },
  
  isTextSchema: function(schemaId) {
    var schema = this.model.schemaMap[schemaId];
    if ((schema != undefined) && (schema.type == "X")){
      return true;
    }
    return false;        
  },
  
  isAssessment: function() {
    return (this.src && this.src == 'resource/x-bb-assessment');
  },

  isAssignment: function() {
    return (this.src && this.src == 'resource/x-bb-assignment');
  },

  getScoreProvider: function() {
    if ( !this.src ) return "";
    return this.model.scoreProvidersMap[ this.src ];
  },
  
  isAllowMulti: function() {
    return (this.am && this.am == "y");
  },

  showGradeDetails: function(userId){
    this.model.showGradeDetails( userId, this.id );
  },

  onAddComment: function(userId){
    this.model.onAddComment( userId, this.id );
  },
  
  exemptGrade: function(userId){
    this.model.exemptGrade( userId, this.id );
  },
  
  clearExemption: function(userId){
    this.model.clearExemption( userId, this.id );
  },

  setComments: function(userId, studentComments, instructorComments){
    this.model.setComments( userId, this.id, studentComments, instructorComments );
  },
  
  onModifyColumn: function( evt ) {
    Event.stop( evt );
    this.model.modifyColumn(this.id,this.type);
  },

  onDeleteColumn: function( evt ) {
    Event.stop( evt );
    if (confirm(this.model.getMessage('confirmDeleteItemMsg'))){
      this.model.deleteColumn(this.id);
    }
  },

  onViewInfo: function(evt) {
    this.cellController.viewColumnInfo(evt,this);
  },
  
  onShowAlignments: function(evt) {
    Event.stop( evt );
    this.cellController.showAlignments( this );
  },

  onShowRubrics: function(evt) {
    Event.stop( evt );
    this.cellController.showRubrics( this );
  },
  
   onItemStats: function ( evt ) {
    Event.stop( evt );    
    this.model.viewItemStats(this.id);
   },

   clearAttemptsByDate: function (startDate, endDate) {
    this.model.clearAttempts(this.id, 'BYDATE', startDate, endDate);
   },

   clearAttempts: function (option) {
    this.model.clearAttempts(this.id, option);
   },

   onViewAssessmentStats: function ( evt ) {
    Event.stop( evt );
    this.model.gradebookService.viewAssessmentStats(this.id);
   },

   onMakeExternalGrade: function ( evt ) {
    Event.stop( evt );
    this.cellController.closePopupsAndRestoreFocus(evt);
    this.model.gradebookService.makeExternalGrade(this.id);
   },
   
   onDownloadAssessmentResults: function ( evt ) {
    Event.stop( evt );
    this.model.gradebookService.downloadAssessmentResults(this.id);
   },

   onAssignmentFileCleanup: function ( evt ) {
    Event.stop( evt );
    this.model.gradebookService.assignmentFileCleanup(this.id);
   },

   onAssignmentDownload: function ( evt ) {
    Event.stop( evt );
    this.model.gradebookService.assignmentDownload(this.id);
   },
   
    onGradeQuestions : function( evt )
    {
      if ( evt ) Event.stop( evt );
      var userIds = this.getFirstFilterMatchUser( false );
      if (userIds == null){
        alert(this.model.getMessage('noUsersFoundAlertMsg'));
        return;
      }
      var url = "/webapps/assessment/do/grade/viewQuestions?outcomeDefinitionId=" + this.id + "&course_id=" + this.model.courseId;
      this.model.gradebookService.gotoURL( url );
    },

    onViewSubmissions : function( evt )
    {
      if ( evt ) Event.stop( evt );
      var userIds = this.getFirstFilterMatchUser( false );
      if (userIds == null){
        alert(this.model.getMessage('noUsersFoundAlertMsg'));
        return;
      }
      var url = "/webapps/assessment/do/viewAttempt?testId=" + this.id + "&course_id=" + this.model.courseId;
      this.model.gradebookService.gotoURL( url );
    },
     
   onGradeAnonymously: function ( evt ) {
    if (evt) Event.stop( evt );
    this._gradeAttempts( true );
   },

   onGradeAttempts: function ( evt ) {
    if (evt) Event.stop( evt );
    this._gradeAttempts( false );
   },

   getFirstFilterMatchUser: function ( anonymousMode ) {
   var grades = this.model._getGradesForItemId(this.id, false /*includeUnavailable*/);
   if (anonymousMode) grades.sort( function() { return (Math.round(Math.random())-0.5); } );
   var filterType = this.model.getCurrentStatus().toUpperCase();
   if (!filterType) filterType = "STAT_ALL";
   if (filterType.startsWith("STAT_")) filterType = filterType.substr(5,filterType.length-5);
   if (filterType == "ALL") filterType = "NN"; // we can't grade null grades
    
    // find first user that has a grade which passes the current filter
   for (var i = 0; i < grades.length; i++){
    if (grades[i].passesFilter(filterType))
      if ( grades[i].isOverride() && !grades[i].hasAttempts() ) continue;
      else return grades[i].getUserId();
   }
   return null;
   },

   _gradeAttempts: function ( anonymousMode ) {
    var userId = this.getFirstFilterMatchUser( anonymousMode );
    if (userId == null){
      alert(this.model.getMessage('noUsersFoundAlertMsg'));
      return;
    }
     // get attempts for user
   var s = this.model.getCurrentStatus();
   if (s.startsWith("stat_")) s = s.substr( 5, status.length - 5 );
     var url = "/webapps/gradebook/do/instructor/getJSONAttemptData?itemId="+this.id+"&course_id="+this.model.courseId+"&userId="+userId+'&status='+s;
   this.model.gradebookService.makeAjaxRequest(url, function ( resp ){
          var attempts = resp.responseJSON;
      if (attempts == null || attempts.length == 0){
        alert(this.model.getMessage('noAttemptsFoundAlertMsg'));
        return;
      }
      // use score provider to get grading page
      // start grading with 1st attempt of user
      var url = this.getGradingPageURL( userId, attempts[0].aid, anonymousMode );
      if ( attempts[0].groupAttemptId != 0 ) url += "&group_attempt_id=" + attempts[0].groupAttemptId; 
      this.model.gradebookService.gotoURL( url );
     }.bind(this));
   },

   getGradingPageURL: function ( userId, attemptId, anonymousMode ) {
    var scoreProvider = this.getScoreProvider();
    var attemptURL = ( scoreProvider?scoreProvider.gradeAction:this.extAttemptHandler );
    var firstSep = '&';
    if (attemptURL.indexOf('?') == -1) {
      firstSep = '?';
    }
    attemptURL = attemptURL.concat(
            firstSep,
            "course_id=", this.model.courseId,
            "&outcomeDefinitionId=", this.id,
            "&courseMembershipId=", userId,
            "&attempt_id=", attemptId,
            "&anonymousMode=", anonymousMode?"true":"false",
            "&cancelGradeUrl=/webapps/gradebook/do/instructor/enterGradeCenter&source=cp_gradebook&linkRefId=", this.linkrefid );
     return attemptURL;
   },

   onShowClearAttemptsForm: function( evt ) {
    this.cellController.showClearAttemptsFlyOut( evt, this.id );
   },
  
  hasContextMenuInfo: function() {
     return true;
  },
  
  getContextMenuInfo: function(cellController) {
    this.cellController = cellController;
    var isAssessment
    var menu = {
      id: "gradeHeaderCM",
      items: [
        {id: "gh_viewColumnInfo", visible:true,
          onclick: this.onViewInfo.bindAsEventListener(this)},
        {id: "gh_showAlignment", visible:this.isAlignable(),
          onclick: this.onShowAlignments.bindAsEventListener(this)},
            {id: "gh_showRubric", visible: ( !this.isCalculated() ),
            onclick: this.onShowRubrics.bindAsEventListener(this)},
        {id: "gh_modifyColumn", visible:true,
          onclick: this.onModifyColumn.bindAsEventListener(this)},
        {id: "gh_columnStats", visible:!this.isTextSchema(this.sid),
          onclick: this.onItemStats.bindAsEventListener(this)},
        {id: "gh_makeExternalGrade", visible:!this.isPublic(),
          onclick: this.onMakeExternalGrade.bindAsEventListener(this)},
        {id: "gh_assessStats", visible:this.isAssessment(),
          onclick: this.onViewAssessmentStats.bindAsEventListener(this)},
        {id: "gh_assessDownload", visible:this.isAssessment(),
          onclick: this.onDownloadAssessmentResults.bindAsEventListener(this)},
        {id: "gh_assignFileCleanup", visible:this.isAssignment(),
          onclick: this.onAssignmentFileCleanup.bindAsEventListener(this)},
        {id: "gh_gradeAttempts", visible:(this.isAssignment() || this.isAssessment()),
          onclick: this.onGradeAttempts.bindAsEventListener(this)},
        {id: "gh_gradeAnonymously", visible:(this.isAssignment() || this.isAssessment()) && !this.hideMenuItem,
          onclick: this.onGradeAnonymously.bindAsEventListener(this)},
        {id: "gh_gradeQuestions", visible:this.isAssessment() && !this.hideMenuItem,
          onclick: this.onGradeQuestions.bindAsEventListener(this)},
        {id: "gh_viewSubmissions", visible:this.isAssessment(),
          onclick: this.onViewSubmissions.bindAsEventListener(this)},
        {id: "gh_assignDownload", visible:this.isAssignment(),
          onclick: this.onAssignmentDownload.bindAsEventListener(this)},
        {id: "gh_clearAllAttempts", visible: this.isAllowMulti(), onclick: this.onShowClearAttemptsForm.bindAsEventListener( this ) },
        {id: "gh_sortAscending", visible:true,
          onclick: this.onSortAscending.bindAsEventListener(this)},
        {id: "gh_sortDescending", visible:true,
          onclick: this.onSortDescending.bindAsEventListener(this)},
        {id: "gh_hideColumn", visible:true,
          onclick: this.onHideColumn.bindAsEventListener(this),
          receipt: 'hideColumnInlineMsg' },
        {id: "gh_deleteColumn", visible:( ( this.isManual() || this.isCalculated() ) && !this.isPublic()), 
          onclick: this.onDeleteColumn.bindAsEventListener(this)},
        {id: "gh_updateColumnStudentVisibility", visible:( !this.isPublic()), 
          onclick: this.onToggleColumnStudentVisibility.bindAsEventListener(this)}
        ]};
    return menu;      
  },
  
  
  getDueDate: function() {
    var dueDate = GradebookUtil.getMessage('noneMsg');
    if (this.due && this.due > 0){ 
      var date = new Date();
      date.setTime(this.due);
      dueDate = formatDate(date,'MMM d, y');
    }
    return dueDate;
  },
  
  // called by item stats page
  getStats: function ( includeUnavailableStudents ) {

    var grades = this.model._getGradesForItemId(this.id, includeUnavailableStudents);
    if (this.primarySchema instanceof Gradebook.TextSchema){
      grades = new Array();
    }
    
    var values = new Array();
    var sum = 0;
    var stats = {};
    stats.count = 0;
    stats.minVal = null;
    stats.maxVal = null;
    stats.qtyNull = 0;
    stats.qtyInProgress = 0;
    stats.qtyNeedsGrading = 0;
    stats.qtyExempt = 0;
    
    for (var i = 0; i < grades.length; i++){
      var grade = grades[i];
      if ( grade.isExcluded( ) ) continue;
      var val = grade.getSortValue();
      var isNull = (val == '-');
      var isIP = grade.attemptInProgress();
      var isNG = grade.needsGrading();
      var isExempt = grade.isExempt();
      var isVal = (!isNull && !isIP && !isNG && !isExempt);
      if (isIP) 
        stats.qtyInProgress++;
      else if (isNG) 
        stats.qtyNeedsGrading++;
      else if (isExempt) 
        stats.qtyExempt++;
      else if (isNull) 
        stats.qtyNull++;
      
      if (isVal){
        if (this.isCalculated()){
          val = (parseFloat(val)/parseFloat(grade.getPointsPossible()) * 100.0);  
        }
        values.push( val );
        sum += parseFloat( val );
        stats.minVal = (stats.minVal == null) ? val : Math.min( val, stats.minVal);
        stats.maxVal = (stats.maxVal == null) ? val : Math.max( val, stats.maxVal);
      }
    }
    stats.count = values.length;
    
    if (values.length == 0 || this.isHideAttemptScore() ){
      stats.avg = '';
      stats.range = '';
      stats.minVal = '';
      stats.maxVal = '';
      stats.median = '';
      stats.variance = '';
      stats.stdDev = '';
    } else {
      stats.avg = sum/values.length;
      stats.range = stats.maxVal - stats.minVal;
      
      values.sort( Gradebook.numberComparator );
      if (values.length == 1){
        stats.median = values[0];
      } else if (values.length % 2){
        // number of values is odd, the median is the middle value
        stats.median = values[parseInt(values.length/2, 10)];
      } else {
        // number of values is even, the median is the average of the two middle values
        stats.median = (values[values.length/2-1] + values[values.length/2])/2;
      }
      stats.variance = this._computeVariance( values, stats.avg );
      stats.stdDev = Math.sqrt( stats.variance );

      stats.maxVal = this._formatFloat( stats.maxVal );
      stats.minVal = this._formatFloat( stats.minVal );
      stats.avg = this._formatFloat( stats.avg );
      stats.range = this._formatFloat( stats.range );
      stats.median = this._formatFloat( stats.median );
      stats.variance = this._formatFloat( stats.variance );
      stats.stdDev = this._formatFloat( stats.stdDev );
    }
    
    stats.gradeDistribution = this.primarySchema.getGradeDistribution( values, this.isCalculated()?100:this.points, stats );
    return stats;
  },

  _formatFloat: function( f ) {
      try {
      if ( f != null ) 
        return NumberFormatter.getDisplayFloat( f.toFixed(2) );
    } catch ( e ) {
      //ignore and return the current value 
    }
    return f;
    
  },

  _computeVariance: function( values, average ) {
    var sumXMeanSquare = 0;
    for (var i = 0; i < values.length; i++){
          var xMean = values[i] - average;
          sumXMeanSquare += (xMean * xMean);
    }
    return sumXMeanSquare / values.length;
  },
  
  getInfo: function() {
    var publicLabel;
    if (this.isPublic())
      publicLabel = GradebookUtil.getMessage('isMsg');
    else
      publicLabel = GradebookUtil.getMessage('isNotMsg');
    var includedInCalculationsLabel;
    if (this.isScorable())
      includedInCalculationsLabel = GradebookUtil.getMessage('yesMsg');
    else
      includedInCalculationsLabel = GradebookUtil.getMessage('noMsg');
    var points = this.getPointsForDisplay();
    var info = [
      {id: "itemInfoId", value: this.getID()},    
      {id: "itemInfoName", value: this.name},
      {id: "itemInfoCategory", value: this.getCategory()},
      {id: "itemInfoSchema", value: this.primarySchema.name},
      {id: "itemInfoPoints", value: (points==0?"-":points)},
      {id: "itemInfoPublic", value: publicLabel},
      {id: "itemInfoIncludedInCalculations", value: includedInCalculationsLabel},
      {id: "itemInfoDueDate", value: this.getDueDate()}
      ];
    return info;
  }
});

Gradebook.StudentAttributeColDef = Class.create();
Object.extend(Gradebook.StudentAttributeColDef.prototype, Gradebook.ColDef.prototype);
Object.extend (Gradebook.StudentAttributeColDef.prototype, {     
  initialize: function(jsonObj, model, schemaMap) {
    Gradebook.ColDef.prototype.initialize.call(this,jsonObj, model, schemaMap);
    this.vis = true;
  },

  isGrade: function() {
    return false;
  },

  isCalculated: function() {
    return false;
  },
  
  isTotal: function() {
    return false;
  },
  
  isWeighted: function() {
    return false;
  },
  
  getType: function() { return "student"; },
  
  // called by GridCell.getCellValue to get value for rendering in spreadsheet
  // format date for last access column, all other columns just return gridcell value
  getCellValue: function( gridCell ){
    var cellVal = gridCell.getValue();
    if (this.id == 'LA'){ // last accessed column
      var dueDate = '';
      if (cellVal && cellVal > 0){ 
        var date = new Date();
        date.setTime(cellVal);
        cellVal = formatDate(date,'MMM d, y');
      }
    }
    return cellVal;
  },

  getRawValue: function( newValue ){
    return newValue;
  },

    updateUserVisibility: function ( userId, visible ) {
        this.model.updateUserVisibility( userId, visible );
    },
    
   
    
  getContextMenuInfo: function(cellController) {
    this.cellController = cellController;
    var canHide = (this.model.colOrderMap[0] != this.model.colDefMap[this.id]);
    var menu = {
      id: "studentInfoHeaderCM",
      items: [
        {id: "sih_sortAscending", visible:true,
          onclick: this.onSortAscending.bindAsEventListener(this)},
        {id: "sih_sortDescending", visible:true,
          onclick: this.onSortDescending.bindAsEventListener(this)},
        {id: "sih_hideColumn", visible:canHide,
          onclick: this.onHideColumn.bindAsEventListener(this), receipt: 'hideColumnInlineMsg' }
        ]};
    return menu;      
  },
  
  hasContextMenuInfo: function() {
    return true;
  }
});
