datum/Datum.js

/* globals window, $, _ */
"use strict";

var FieldDBObject = require("./../FieldDBObject").FieldDBObject;
var AudioVideo = require("./../audio_video/AudioVideo").AudioVideo;
var AudioVideos = require("./../audio_video/AudioVideos").AudioVideos;
var Comments = require("./../comment/Comments").Comments;
var DatumField = require("./DatumField").DatumField;
var DatumFields = require("./DatumFields").DatumFields;
// var DatumState = require("./../FieldDBObject").FieldDBObject;
var DatumStates = require("./DatumStates").DatumStates;
// var DatumTag = require("./../FieldDBObject").FieldDBObject;
var DatumTags = require("./DatumTags").DatumTags;
var Images = require("./../image/Images").Images;
var Session = require("./Session").Session;

var DEFAULT_CORPUS_MODEL = require("./../corpus/corpus.json");

/**
 * @class The Datum model which contains metadata to help validate and search data
 *
 * @property {AudioVisual} audioVideo Datums can be associated with an audio or video
 *           file.
 * @property {Session} session The session provides details about the set of
 *           data elicited. The session will contain details such as date,
 *           language, consultant etc.
 * @property {Comments} comments The comments is a collection of comments
 *           associated with the datum, this is meant for comments like on a
 *           blog, not necessarily notes, which can be encoded in a
 *           field.(Use Case: team discussing a particular datum)
 * @property {DatumTags} datumtags The datum tags are a collection of tags
 *           associated with the datum. These are made completely by the
 *           user.They are like blog tags, a way for the user to make
 *           categories without make a hierarchical structure, and make
 *           datum easier for search.
 * @property {Date} dateEntered The date the Datum was first saved.
 * @property {Date} dateModified The date the Datum was last saved.
 *
 * @description The initialize function brings up the datum widget in small
 *              view with one set of datum fields. However, the datum widget
 *              can contain more than datum field set and can also be viewed
 *              in full screen mode.
 *
 * @name  Datum
 * @extends FieldDBObject
 * @constructs
 */
var Datum = function Datum(options) {
  if (!this._fieldDBtype) {
    this._fieldDBtype = "Datum";
  }
  this.debug("Constructing Datum: ", options);
  FieldDBObject.apply(this, arguments);
};

Datum.prototype = Object.create(FieldDBObject.prototype, /** @lends Datum.prototype */ {
  constructor: {
    value: Datum
  },

  fields: {
    get: function() {
      this.debug("getting fields");
      if (!this._fields && this.corpus && typeof this.corpus.updateDatumToCorpusFields === "function") {
        this.corpus.updateDatumToCorpusFields(this);
      }
      return this._fields;
    },
    set: function(value) {
      if (value && !value.confidential && this.confidential) {
        value.confidential = this.confidential;
      }
      this.ensureSetViaAppropriateType("fields", value);
    }
  },

  datumFields: {
    get: function() {
      this.debug("datumFields is depreacted, just use fields instead");
      return this.fields;
    },
    set: function(value) {
      this.debug("datumFields is depreacted, just use fields instead");
      return this.fields = value;
    }
  },

  accessAsObject: {
    get: function() {
      var obj = {};

      var passValueReference = function(field) {
        obj[field.id] = field.value;
      };

      this.fields.map(passValueReference);
      if (this.session) {
        this.session.fields.map(passValueReference);
      } else {
        this.warn("this datum is missing a session, this is stange");
      }

      obj.comments = this.comments.map(function(comment) {
        return comment.text;
      }).join("; ");
      obj.audioVideo = this.audioVideo.map(function(audioVideo) {
        return audioVideo.filename;
      }).join("; ");
      obj.images = this.images.map(function(image) {
        return image.filename;
      }).join("; ");
      return obj;
    },
    set: function() {}
  },

  addField: {
    value: function(field) {
      if (!this.fields) {
        this.fields = [];
      }
      if (typeof field === "string") {
        // this.fields.debugMode = true;
        if (this.fields[field]) {
          return this.fields[field];
        }
        field = {
          id: field
        };
      }
      this.debug("adding field", field);
      return this.fields.add(field);
    }
  },

  addFile: {
    value: function(newFileDetails) {
      if (!newFileDetails) {
        this.warn("A null file was requested to be added to this datum", newFileDetails);
        return;
      }
      newFileDetails.type = newFileDetails.type || "";
      if (newFileDetails.type.indexOf("audio") === 0) {
        this.audioVideo = this.audioVideo || [];
        this.audioVideo.add(newFileDetails);
      } else if (newFileDetails.type.indexOf("video") === 0) {
        this.audioVideo = this.audioVideo || [];
        this.audioVideo.add(newFileDetails);
      } else if (newFileDetails.type.indexOf("image") === 0) {
        this.images = this.images || [];
        this.images.add(newFileDetails);
      } else {
        var regularizedJSON = new AudioVideo(newFileDetails).toJSON();
        this.addRelatedData(regularizedJSON);
      }
      this.unsaved = true;
    }
  },

  audioVideo: {
    get: function() {
      if (this._audioVideo && this._audioVideo.fieldDBtype === "AudioVideos") {
        this._audioVideo.dbname = this.dbname;
      }
      return this._audioVideo || FieldDBObject.DEFAULT_COLLECTION;
    },
    set: function(value) {
      this.ensureSetViaAppropriateType("audioVideo", value);
    }
  },

  hasAudio: {
    get: function() {
      var hasAudio = false;
      if (this.audioVideo.length > 0) {
        this.audioVideo.map(function(audioVideo) {
          if (audioVideo.trashed !== "deleted") {
            hasAudio = true;
          }
        });
      }
      return hasAudio;
    },
    set: function() {}
  },

  play: {
    value: function(optionalIndex) {
      this.debug("optionalIndex", optionalIndex);
      if (this._audioVideo && typeof this._audioVideo.play === "function") {
        this._audioVideo.play(0);
      }
    }
  },

  images: {
    get: function() {
      if (this._images && this._images.fieldDBtype === "Images") {
        this._images.dbname = this.dbname;
      }
      return this._images || FieldDBObject.DEFAULT_COLLECTION;
    },
    set: function(value) {
      if (value && !value.confidential && this.confidential) {
        value.confidential = this.confidential;
      }
      this.ensureSetViaAppropriateType("images", value);
    }
  },

  hasImages: {
    get: function() {
      var hasImages = false;
      if (this.images.length > 0) {
        this.images.map(function(image) {
          if (image.trashed !== "deleted") {
            hasImages = true;
          }
        });
      }
      return hasImages;
    },
    set: function() {}
  },

  relatedData: {
    get: function() {
      if (!this.fields) {
        return;
      }
      if (this.fields && !this.fields.relatedData && DEFAULT_CORPUS_MODEL && DEFAULT_CORPUS_MODEL.datumFields && DEFAULT_CORPUS_MODEL.datumFields.length >= 10) {
        this.fields.add(DEFAULT_CORPUS_MODEL.datumFields[10]);
      }
      this.fields.relatedData.json = this.fields.relatedData.json || {};
      this.fields.relatedData.json.relatedData = this.fields.relatedData.json.relatedData || [];
      return this.fields.relatedData.json.relatedData;
    },
    set: function() {
      // if (this.fields && this.fields.relatedData) {
      //   // this.fields.debugMode = true;
      // } else {
      //   return;
      // }
      // // this.fields.relatedData.json = this.fields.relatedData.json || {};
      // // this.fields.relatedData.json.relatedData = value;
    }
  },

  // The couchdb-connector is capable of mapping the url scheme
  // proposed by the authors of Backbone to documents in your database,
  // so that you don't have to change existing apps when you switch the sync-strategy
  api: {
    value: "datums"
  },

  // Internal models: used by the parse function
  INTERNAL_MODELS: {
    value: {
      fields: DatumFields,
      audioVideo: AudioVideos,
      session: Session,
      comments: Comments,
      datumStates: DatumStates,
      datumTags: DatumTags,
      images: Images
    }
  },

  fillWithCorpusFieldsIfMissing: {
    value: function() {
      if (!this.fields) {
        return;
      }
      /* Update the datum to show all fields which are currently in the corpus, they are only added if saved. */
      var corpusFields = window.app.corpus.datumFields.models;
      for (var field in corpusFields) {
        var label = corpusFields[field].get("label");
        this.debug("Label " + label);
        var correspondingFieldInThisDatum = this.fields.where({
          label: label
        });
        if (correspondingFieldInThisDatum.length === 0) {
          this.fields.push(corpusFields[field]);
        }
      }
    }
  },
  search: {
    value: function(queryString, limitToOptionalFields) {
      try {
        //http://support.google.com/analytics/bin/answer.py?hl=en&answer=1012264
        window.pageTracker._trackPageview("/search_results.php?q=" + queryString);
      } catch (e) {
        this.debug("Search Analytics not working.");
      }

      // Process the given query string into tokens
      var queryTokens = this.processQueryString(queryString);
      var doGrossKeywordMatch = false;
      if (queryString.indexOf(":") === -1) {
        doGrossKeywordMatch = true;
        queryString = queryString.toLowerCase().replace(/\s/g, "");
      }

      if (true) {
        var accessAsObject = this.accessAsObject;
        // If the caller asked for only certain fields, search only those.
        if (limitToOptionalFields) {
          this.debug("Reducing fields to the fields requested by the user", limitToOptionalFields);
          for (var fieldlabel in limitToOptionalFields) {
            if (!limitToOptionalFields.hasOwnProperty(fieldlabel) || !limitToOptionalFields[fieldlabel]) {
              continue;
            }
            limitToOptionalFields[fieldlabel] = accessAsObject[fieldlabel];
          }
        } else {
          limitToOptionalFields = accessAsObject;
        }

        var highlightedMatchesInHTML = this.highlightMatches(limitToOptionalFields, queryString, doGrossKeywordMatch, queryTokens);
        // If the row's datum matches the given query string
        if (highlightedMatchesInHTML && highlightedMatchesInHTML.length > 0) {
          // Keep its datum's ID, which is the value
          this.highlightedMatches = highlightedMatchesInHTML; // ["TODO showing the matched with a window of 20 char before and after."];
          return this.highlightedMatches;
        }
        delete this.highlightedMatches;
        return;
      }

      var self = this;
      try {
        self.pouch(function(err, db) {
          db.query("pages/get_datum_fields", {
            reduce: false
          }, function(err, response) {
            var matchIds = [];

            if (!err) {

              // Go through all the rows of results
              for (var i in response.rows) {
                var thisDatumIsIn = self.highlightMatches(response.rows[i], queryString, doGrossKeywordMatch, queryTokens);
                // If the row's datum matches the given query string
                if (thisDatumIsIn) {
                  // Keep its datum's ID, which is the value
                  matchIds.push(response.rows[i].value);
                }
              }
            } else {
              if (window.toldSearchtomakeviews) {
                self.debug("Told search to make views once, apparently it didnt work. Stopping it from looping.");
                return;
              }
              /*
               * Its possible that the corpus has no search views, create them and then try searching again.
               */
              window.appView.toastUser("Initializing your search functions for the first time." +
                " Search is pretty powerful, " +
                " in fact if you're the power user type you can write your " +
                "own data extracting/filtering/visualization queries using " +
                " <a href='http://www.kchodorow.com/blog/2010/03/15/mapreduce-the-fanfiction/' target='_blank'>MapReduce.</a>", "alert-success", "Search:");
              window.toldSearchtomakeviews = true;
              var previousquery = queryString;
              window.app.corpus.createPouchView("pages/get_datum_fields", function() {
                window.appView.searchEditView.search(previousquery);
              });
            }

          });
        });
      } catch (e) {
        self.bug("Couldnt search the data, if you sync with the server you might get the most recent search index.");
      }
    }
  },
  highlightMatches: {
    value: function(simpleObject, queryString, doGrossKeywordMatch, queryTokens) {
      var fieldlabel,
        highlightedmatch;

      this.debug("Highlighting matches for ", simpleObject, queryString, doGrossKeywordMatch, queryTokens);
      var highlightedMatches = [];
      // If the query string is null, include all datumIds
      if (queryString.trim() === "") {
        this.debug("user searched for nothing, which means we will include this item.");
        highlightedMatches = [" "];
      } else if (doGrossKeywordMatch) {
        this.debug("user searched for GrossKeywordMatch of " + queryString);

        for (fieldlabel in simpleObject) {
          if (!simpleObject.hasOwnProperty(fieldlabel) || !simpleObject[fieldlabel]) {
            continue;
          }
          highlightedmatch = this.highlight(simpleObject[fieldlabel], queryString);
          if (highlightedmatch.indexOf("<span") > -1) {
            highlightedMatches.push(highlightedmatch);
          }
        }
        return highlightedMatches;
      } else {

        var conditionalHighlightedMatches = [];
        this.debug(" Searching through ", queryTokens);

        // Determine if this datum matches the first search criteria
        highlightedmatch = this.matchesSingleCriteria(simpleObject, queryTokens[0]);
        if (highlightedmatch && highlightedmatch.indexOf("<span") > -1) {
          this.debug("      Found one match  " + highlightedmatch);

          conditionalHighlightedMatches.push(highlightedmatch);
        }
        // Progressively determine whether the datum still matches based on
        // subsequent search criteria
        for (var j = 1; j < queryTokens.length; j += 2) {
          if (queryTokens[j] === "AND") {
            // Do an intersection, if this fails, exit early.
            highlightedmatch = this.matchesSingleCriteria(simpleObject, queryTokens[j + 1]);
            if (highlightedmatch && highlightedmatch.indexOf("<span") > -1) {
              this.debug("       Passed intersection match  " + highlightedmatch);
              conditionalHighlightedMatches.push(highlightedmatch);
            } else {
              this.debug("       Failed the Intersection match " + highlightedmatch);
              return;
            }
          } else {
            // Do a union
            highlightedmatch = this.matchesSingleCriteria(simpleObject, queryTokens[j + 1]);
            if (highlightedmatch && highlightedmatch.indexOf("<span") > -1) {
              this.debug("      Found another match  ", highlightedmatch);
              conditionalHighlightedMatches.push(highlightedmatch);
            }
          }
        }

        if (conditionalHighlightedMatches && conditionalHighlightedMatches.length > 0) {
          this.debug("This datum matches ", conditionalHighlightedMatches);
          return conditionalHighlightedMatches;
        }
        this.debug("This datum doesnt match ", queryTokens);

      }
      return highlightedMatches;
    }
  },
  /**
   * Determines whether the given object to search through matches the given
   * search criteria.
   *
   * @param {Object} objectToSearchThrough An object representing a datum that
   * contains (key, value) pairs where the key is the datum field label and the
   * value is the datum field value of that attribute.
   * @param {String} criteria The single search criteria in the form of a string
   * made up of a label followed by a colon followed by the value that we wish
   * to match.
   *
   * @return {Boolean} True if the given object matches the given criteria.
   * False otherwise.
   */
  matchesSingleCriteria: {
    value: function(objectToSearchThrough, criteria) {
      var delimiterIndex = criteria.indexOf(":");
      var label = criteria.substring(0, delimiterIndex);
      var negate = false;
      if (label.indexOf("!") === 0) {
        label = label.replace(/^!/, "");
        negate = true;
      }
      var value = criteria.substring(delimiterIndex + 1);

      this.debug("    Looking at label:" + label + ": value :" + value + ": in :" + objectToSearchThrough[label]);
      /* handle the fact that "" means grammatical, so if user asks for  specifically, give only the ones wiht empty judgemnt */
      if (label === "judgement" && value.toLowerCase() === "grammatical") {
        if (!objectToSearchThrough[label]) {
          return this.highlight("Grammatical", "Grammatical");
        }
      }

      var searchResult = objectToSearchThrough[label];
      searchResult = this.highlight(searchResult, value);

      if (negate) {
        if (searchResult.indexOf("<span") > -1) {
          searchResult = "";
        } else {
          searchResult = this.highlight(searchResult, searchResult);
        }
      }

      return searchResult;
    }
  },

  /**
   * Process the given string into an array of tokens where each token is
   * either a search criteria or an operator (AND or OR). Also makes each
   * search criteria token lowercase, so that searches will be case-
   * insensitive.
   *
   * @param {String} queryString The string to tokenize.
   *
   * @return {String} The tokenized string
   */
  processQueryString: {
    value: function(queryString) {
      if (!queryString) {
        return;
      }
      queryString = queryString + "";
      queryString = queryString.replace(/\&\&/g, " AND ").replace(/\|\|/g, " OR ");
      this.debug("    Normalized  query string, " + queryString);

      // Split on spaces
      var queryArray = queryString.split(/ +/);

      // Create an array of tokens out of the query string where each token is
      // either a search criteria or an operator (AND or OR).
      var queryTokens = [];
      var currentString = "";
      for (var i in queryArray) {
        var currentItem = queryArray[i].trim();
        if (currentItem.length <= 0) {
          break;
        } else if ((currentItem === "AND") || (currentItem === "OR")) {
          queryTokens.push(currentString);
          queryTokens.push(currentItem);
          currentString = "";
        } else if (currentString) {
          /* toLowerCase introduces a bug in search where camel case fields loose their capitals, then cant be matched with fields in the map reduce results */
          currentString = currentString + " " + currentItem; //.toLowerCase();
        } else {
          currentString = currentItem; //.toLowerCase();
        }
      }
      queryTokens.push(currentString);

      this.debug("    Tokenized query string, ", queryTokens);
      return queryTokens;
    }
  },
  getDisplayableFieldForActivitiesEtc: {
    value: function() {
      return this.model.fields.where({
        label: "utterance"
      })[0].get("mask");
    }
  },
  /**
   * Clone the current Datum and return the clone. The clone is put in the current
   * Session, regardless of the origin Datum's Session. //TODO it doesn tlook liek this is the case below:
   *
   * @return The clone of the current Datum.
   */
  cloneDeprecated: {
    value: function() {
      // Create a new Datum based on the current Datum
      var datum = new Datum({
        audioVideo: new AudioVideos(this.get("audioVideo").toJSON(), {
          parse: true
        }),
        comments: new Comments(this.get("comments").toJSON(), {
          parse: true
        }),
        dateEntered: this.get("dateEntered"),
        dateModified: this.get("dateModified"),
        fields: new DatumFields(this.fields.toJSON(), {
          parse: true
        }),
        datumStates: new DatumStates(this.get("datumStates").toJSON(), {
          parse: true
        }),
        datumTags: new DatumTags(this.get("datumTags").toJSON(), {
          parse: true
        }),
        dbname: this.dbname,
        session: this.get("session")
      });

      return datum;
    }
  },

  /**
   * This function is used to get the most prominent datumstate (now called
   * ValidationStatus) eg "CheckedWithSeberina" or "Deleted" or "ToBeChecked"
   *
   * @returns {String} a string which is the first item in the
   *          validationSatuts field
   */
  getValidationStatus: {
    value: function() {
      var validationStatus = "";
      var stati = this.fields.where({
        "label": "validationStatus"
      });
      if (stati.length > 0) {
        stati = stati[0].get("mask").split(" ");
        if (stati.length > 0) {
          validationStatus = stati[0];
        }
      }
      /* Handle upgrade from previous corpora look in datum states too */
      if (validationStatus === "") {
        stati = this.get("datumStates").where({
          selected: "selected"
        });
        if (stati.length > 0) {
          validationStatus = stati[0].get("state");
        }
      }
      this.updateDatumState(validationStatus);
      return validationStatus;
    }
  },
  /**
   * This function is used to colour a datum background to make
   * visually salient the validation status of the datum.
   *
   * @param status
   *            This is an optional string which is used to find the
   *            colour for a particular DatumState. If the string is
   *            not provided it gets the first element from the
   *            validation status field.
   * @returns {String} This is the colour using Bootstrap (warning is
   *          Orange, success Green etc.)
   */
  getValidationStatusColor: {
    value: function(status) {
      if (!status) {
        status = this.getValidationStatus();
      }
      /* TODO once the new ValidationStatus pattern is in the corpus proper, dont hard code the colors */
      if (status.toLowerCase().indexOf("deleted") > -1) {
        return "danger";
      }
      if (status.toLowerCase().indexOf("tobechecked") > -1) {
        return "warning";
      }
      if (status.toLowerCase().indexOf("checked") > -1) {
        return "success";
      }
    }
  },

  /**
   * This function is used to set the primary status of the datum,
   * eg. put Deleted as the first item in the validation status.
   *
   * @param selectedValue
   *            This is a string which is the validation status
   *            you want the datum to be
   */
  updateDatumState: {
    value: function(selectedValue) {
      if (!selectedValue) {
        return;
      }
      this.debug("Asking to change the datum state to " + selectedValue);
      /* make sure all the corpus states are availible in this datum */
      var thisdatumStates = this.get("datumStates");
      window.app.corpus.get("datumStates").each(function(datumstate) {
        var obj = datumstate.toJSON();
        obj.selected = "";
        thisdatumStates.addIfNew(obj);
      });
      try {
        $.each(this.get("datumStates").where({
          selected: "selected"
        }), function() {
          if (this.get("state") !== selectedValue) {
            this.set("selected", "");
          }
        });
        this.get("datumStates").where({
          state: selectedValue
        })[0].set("selected", "selected");
      } catch (e) {
        this.debug("problem getting color of datum state, probaly none are selected.", e);
      }

      /* prepend this state to the new validationStates as of v1.46.2 */
      var n = this.fields.where({
        label: "validationStatus"
      })[0];
      if (n === [] || !n) {
        n = new DatumField({
          label: "validationStatus",
          shouldBeEncrypted: "",
          showToUserTypes: "all",
          userchooseable: "disabled",
          help: "Any number of status of validity (replaces DatumStates). For example: ToBeCheckedWithSeberina, CheckedWithRicardo, Deleted etc..."
        });
        this.fields.add(n);
      }
      var validationStatus = n.get("mask") || "";
      validationStatus = selectedValue + " " + validationStatus;
      var uniqueStati = _.unique(validationStatus.trim().split(" "));
      n.set("mask", uniqueStati.join(" "));

      //      this.save();
      //TODO save it
    }
  },

  /**
   * Make the  model marked as Deleted, mapreduce function will
   * ignore the deleted models so that it does not show in the app,
   * but deleted model remains in the database until the admin empties
   * the trash.
   *
   * Also remove it from the view so the user cant see it.
   *
   */
  putInTrash: {
    value: function() {
      this.set("trashed", "deleted" + Date.now());
      this.updateDatumState("Deleted");
      this.saveAndInterConnectInApp(function() {
        /* This actually removes it from the database */
        //thisdatum.destroy();
        if (window.appView) {
          window.appView.datumsEditView.showMostRecentDatum();
        }
      });
    }
  },

  /**
   * The LaTeXiT function automatically mark-ups an example in LaTeX code
   * (\exg. \"a) and then copies it on the export modal so that when the user
   * switches over to their LaTeX file they only need to paste it in.
   *
   * We did a poll on Facebook among EGGers, and other linguists we know and
   * found that Linguex was very popular, and GB4E, so we did the export in
   * GB4E.
   */
  /* jshint ignore:start */

  laTeXiT: {
    value: function(showInExportModal) {
      this.debug(showInExportModal);
      //corpus's most frequent fields
      var frequentFields;
      if (this.application && this.application.corpus && this.application.corpus.frequentFields) {
        frequentFields = this.application.corpus.frequentFields;
      } else {
        frequentFields = this.fields.map(function(field) {
          return field.id
        });
      }
      //this datum/datalist's datumfields and their names
      var fields = this.fields.map(function(field) {
        return field.value;
      });
      var fieldLabels = this.fields.map(function(field) {
        return field.id;
      });

      var result = "\n";

      //remove any empty fields from our arrays
      for (var i = fields.length - 1; i >= 0; i--) {
        if (!fields[i]) {
          fields.splice(i, 1);
          fieldLabels.splice(i, 1);
        }
      }
      /*throughout this next section, print frequent fields and infrequent ones differently
      frequent fields get latex'd as items in a description and infrequent ones are the same,
      but commented out.*/
      if (fields && (fields.length > 0)) {
        var numInfrequent = 0;
        for (var field in fields) {
          if (frequentFields.indexOf(fieldLabels[field]) >= 0) {
            break;
          }
          numInfrequent++;
        }
        if (numInfrequent !== fieldLabels.length) {
          result = result + "\n  \\begin\{description\}";
        } else {
          result = result + "\n%  \\begin\{description\}";
        }
        for (field in fields) {
          if (fields[field] && (frequentFields.indexOf(fieldLabels[field]) >= 0)) {
            result = result + "\n    \\item\[\\sc\{" + this.escapeLatexChars(fieldLabels[field]) + "\}\] " + this.escapeLatexChars(fields[field]);
          } else if (fields[field]) {
            /* If as a field that is designed for LaTex dont excape the LaTeX characters */
            if (fieldLabels[field].toLowerCase().indexOf("latex") > -1) {
              result = result + "\n " + fields[field];
            } else {
              result = result + "\n%    \\item\[\\sc\{" + this.escapeLatexChars(fieldLabels[field]) + "\}\] " + this.escapeLatexChars(fields[field]);
            }
          }
        }
        if (numInfrequent !== fieldLabels.length) {
          result = result + "\n  \\end\{description\}";
        } else {
          result = result + "\n%  \\end\{description\}";
        }

      }
      result = result + "\n\\end{exe}\n\n";

      return result;
    }
  },
  /* jshint ignore:end */

  latexitDataList: {
    value: function(showInExportModal) {
      //this version prints new data as well as previously shown latex'd data (best for datalists)
      var result = this.laTeXiT(showInExportModal);
      if (showInExportModal) {
        $("#export-type-description").html(" as <a href='http://latex.informatik.uni-halle.de/latex-online/latex.php?spw=2&id=562739_bL74l6X0OjXf' target='_blank'>LaTeX (GB4E)</a>");
        $("#export-text-area").val($("#export-text-area").val() + result);
      }
      return result;
    }
  },

  latexitDatum: {
    value: function(showInExportModal) {
      //this version prints new data and deletes previously shown latex'd data (best for datums)
      var result = this.laTeXiT(showInExportModal);
      if (showInExportModal) {
        $("#export-type-description").html(" as <a href='http://latex.informatik.uni-halle.de/latex-online/latex.php?spw=2&id=562739_bL74l6X0OjXf' target='_blank'>LaTeX (GB4E)</a>");
        var latexDocument =
          "\\documentclass[12pt]{article} \n" +
          "\\usepackage{fullpage} \n" +
          "\\usepackage{tipa} \n" +
          "\\usepackage{qtree} \n" +
          "\\usepackage{gb4e} \n" +
          "\\begin{document} \n" + result +
          "\\end{document}";
        $("#export-text-area").val(latexDocument);
      }
      return result;
    }
  },

  escapeLatexChars: {
    value: function(input) {
      var result = input;
      //curly braces need to be escaped TO and escaped FROM, so we're using a placeholder
      result = result.replace(/\\/g, "\\textbackslashCURLYBRACES");
      result = result.replace(/\^/g, "\\textasciicircumCURLYBRACES");
      result = result.replace(/\~/g, "\\textasciitildeCURLYBRACES");
      result = result.replace(/#/g, "\\#");
      result = result.replace(/\$/g, "\\$");
      result = result.replace(/%/g, "\\%");
      result = result.replace(/&/g, "\\&");
      result = result.replace(/_/g, "\\_");
      result = result.replace(/{/g, "\\{");
      result = result.replace(/}/g, "\\}");
      result = result.replace(/</g, "\\textless");
      result = result.replace(/>/g, "\\textgreater");

      result = result.replace(/CURLYBRACES/g, "{}");
      return result;
    }
  },

  datumIsInterlinearGlossText: {
    value: function(fieldLabels) {
      if (!fieldLabels) {
        fieldLabels = this.fields.map(function(field) {
          return field.id;
        });
      }

      var utteranceOrMorphemes = false;
      var gloss = false;
      var trans = false;
      for (var fieldLabel in fieldLabels) {
        if (fieldLabels[fieldLabel] === "utterance" || fieldLabels[fieldLabel] === "morphemes") {
          utteranceOrMorphemes = true;
        }
        if (fieldLabels[fieldLabel] === "gloss") {
          gloss = true;
        }
        if (fieldLabels[fieldLabel] === "translation") {
          trans = true;
        }
      }
      if (gloss || utteranceOrMorphemes || trans) {
        return true;
      } else {
        return false;
      }
    }
  },

  /**
   * This function simply takes the utterance gloss and translation and puts
   * them out as plain text so the user can do as they wish.
   */
  exportAsPlainText: {
    value: function(showInExportModal) {
      // var header = _.pluck(this.fields.toJSON(), "label") || [];
      var fields = this.fields.map(function(field) {
        return field.value;
      });
      var result = fields.join("\n");
      if (showInExportModal) {
        $("#export-type-description").html(" as text (Word)");
        $("#export-text-area").val(
          $("#export-text-area").val() + result
        );
      }
      return result;
    }
  },

  /**
   * This takes as an argument the order of fields and then creates a row of csv.
   */
  exportAsCSV: {
    value: function(showInExportModal, orderedFields, printheaderonly) {
      var asFlatObject = this.accessAsObject,
        header = orderedFields || [],
        columns = [];

      if (orderedFields) {
        columns = orderedFields.map(function(fieldid) {
          return asFlatObject[fieldid] || "";
        });
      } else {
        for (var fieldlabel in asFlatObject) {
          header.push(fieldlabel);
          columns.push(asFlatObject[fieldlabel]);
        }
      }
      if (printheaderonly) {
        columns = header;
      }
      var result = columns.map(function(cell) {
        return "\"" + cell.replace(/"/g, "\\\"") + "\"";
      }).join(",") + "\n";

      //      if (orderedFields === null) {
      //        orderedFields = ["judgement","utterance","morphemes","gloss","translation"];
      //      }
      //      judgement = this.columns.where({label: "judgement"})[0].get("mask");
      //      morphemes = this.columns.where({label: "morphemes"})[0].get("mask");
      //      utterance= this.columns.where({label: "utterance"})[0].get("mask");
      //      gloss = this.columns.where({label: "gloss"})[0].get("mask");
      //      translation= this.columns.where({label: "translation"})[0].get("mask");
      //      var resultarray =  [judgement,utterance,morphemes,gloss,translation];
      //      var result = '"' + resultarray.join('","') + '"\n';

      if (showInExportModal) {
        $("#export-type-description").html(" as CSV (Excel, Filemaker Pro)");
        $("#export-text-area").val(
          $("#export-text-area").val() + result);
      }
      return result;
    }
  },

  /**
   * Encrypts the datum if it is confidential
   *
   * @returns {Boolean}
   */
  encrypt: {
    value: function() {
      this.set("confidential", true);
      this.fields.each(function(dIndex) {
        dIndex.set("encrypted", "checked");
      });
      //TODO scrub version history to get rid of all unencrypted versions.
      this.saveAndInterConnectInApp(window.app.router.renderDashboardOrNot, window.app.router.renderDashboardOrNot);
    }
  },

  /**
   * Decrypts the datum if it was encrypted
   */
  decrypt: {
    value: function() {
      this.set("confidential", false);

      this.fields.each(function(dIndex) {
        dIndex.set("encrypted", "");
      });
    }
  },
  /**
   * Accepts two functions to call back when save is successful or
   * fails. If the fail callback is not overridden it will alert
   * failure to the user.
   *
   * - Adds the datum to the top of the default data list in the corpus if it is in the right corpus
   * - Adds the datum to the datums container if it wasnt there already
   * - Adds an activity to the logged in user with diff in what the user changed.
   *
   * @param successcallback
   * @param failurecallback
   */
  saveAndInterConnectInApp: {
    value: function(successcallback) {
      if (typeof successcallback === "function") {
        successcallback();
      }
    }
  },
  /**
   * Accepts two functions success will be called if sucessfull,
   * otherwise it will attempt to render the current datum views. If
   * the datum isn't in the current corpus it will call the fail
   * callback or it will alert a bug to the user. Override the fail
   * callback if you don't want the alert.
   *
   * @param successcallback
   * @param failurecallback
   * @deprecated
   */
  setAsCurrentDatum: {
    value: function() {
      this.warn("Using deprected method setAsCurrentDatum.");
      //      if( window.app.corpus.dbname !== this.dbname ){
      //        if (typeof failurecallback === "function") {
      //          failurecallback();
      //        }else{
      //          self.bug("This is a bug, cannot load the datum you asked for, it is not in this corpus.");
      //        }
      //        return;
      //      }else{
      //        if (window.appView.datumsEditView.datumsView.collection.models[0].id !== this.id ) {
      //          window.appView.datumsEditView.datumsView.prependDatum(this);
      //          //TODO might not need to do it on the Read one since it is the same model?
      //        }
      //        if (typeof successcallback === "function") {
      //          successcallback();
      //        }
      //      }
    }
  },

  /* highlight returns text with all instances of stringToHighlight enclosed
   * in a span.  Note that stringToHighlight is treated as a regexp.
   */
  highlight: {
    value: function(text, stringToHighlight, className) {
      className = className || "highlight";
      var re = new RegExp("(" + stringToHighlight + ")", "gi");
      return text.replace(re, "<span class='" + className + "'>$1</span>");
    }
  },

  toJSON: {
    value: function(includeEvenEmptyAttributes, removeEmptyAttributes) {
      this.debug("Customizing toJSON ", includeEvenEmptyAttributes, removeEmptyAttributes);

      var json = FieldDBObject.prototype.toJSON.apply(this, arguments);
      if (!json) {
        this.warn("Not returning json right now.");
        return;
      }

      this.debug("saving fields as the deprecated datumFields");
      json.datumFields = json.fields;
      delete json.fields;

      this.debug(json);
      return json;
    }
  }

});
exports.Datum = Datum;