corpus/Corpus.js

/* global window, OPrime */
var Confidential = require("./../confidentiality_encryption/Confidential").Confidential;
var CorpusMask = require("./CorpusMask").CorpusMask;
var LanguageDatum = require("./../datum/LanguageDatum").LanguageDatum;
var DatumField = require("./../datum/DatumField").DatumField;
var DatumFields = require("./../datum/DatumFields").DatumFields;
var Session = require("./../datum/Session").Session;
var Speaker = require("./../user/Speaker").Speaker;
var FieldDBObject = require("./../FieldDBObject").FieldDBObject;
var Q = require("q");

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

/**
 * @class A corpus is like a git repository, it has a remote, a title
 *        a description and perhaps a readme When the user hits sync
 *        their "branch" of the corpus will be pushed to the central
 *        remote, and we will show them a "diff" of what has
 *        changed.
 *
 * The Corpus may or may not be a git repository, so this class is
 * to abstract the functions we would expect the corpus to have,
 * regardless of how it is really stored on the disk.
 *
 *
 * @property {String} title This is used to refer to the corpus, and
 *           what appears in the url on the main website eg
 *           http://fieldlinguist.com/LingLlama/SampleFieldLinguisticsCorpus
 * @property {String} description This is a short description that
 *           appears on the corpus details page
 * @property {String} remote The git url of the remote eg:
 *           git@fieldlinguist.com:LingLlama/SampleFieldLinguisticsCorpus.git
 *
 * @property {Consultants} consultants Collection of consultants who contributed to the corpus
 * @property {DatumStates} datumstates Collection of datum states used to describe the state of datums in the corpus
 * @property {DatumFields} datumFields Collection of datum fields used in the corpus
 * @property {ConversationFields} conversationfields Collection of conversation-based datum fields used in the corpus
 * @property {Sessions} sessions Collection of sessions that belong to the corpus
 * @property {DataLists} datalists Collection of data lists created under the corpus
 * @property {Permissions} permissions Collection of permissions groups associated to the corpus
 *
 *
 * @property {Glosser} glosser The glosser listens to
 *           orthography/utterence lines and attempts to guess the
 *           gloss.
 * @property {Lexicon} lexicon The lexicon is a list of morphemes,
 *           allomorphs and glosses which are used to index datum, and
 *           also to gloss datum.
 *
 * @description The initialize function probably checks to see if
 *              the corpus is new or existing and brings it down to
 *              the user's client.
 *
 * @extends CorpusMask
 * @tutorial tests/corpus/CorpusTest.js
 */

var Corpus = function Corpus(options) {
  if (!this._fieldDBtype) {
    this._fieldDBtype = "Corpus";
  }
  this.debug("Constructing corpus", options);
  CorpusMask.apply(this, arguments);
};
Corpus.DEFAULT_DATUM = LanguageDatum;

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

  /**
   *  Must customize id to the original method since CorpusMask overrides it with "corpus"
   */
  id: {
    get: function() {
      return this._id || FieldDBObject.DEFAULT_STRING;
    },
    set: function(value) {
      if (value === this._id) {
        return;
      }
      if (!value) {
        delete this._id;
        return;
      }
      if (value.trim) {
        value = value.trim();
      }
      this._id = value;
    }
  },

  dateOfLastDatumModifiedToCheckForOldSession: {
    get: function() {
      var timestamp = 0;
      if (this.sessions && this.sessions.length > 0) {
        var mostRecentSession = this.sessions[this.sessions.length - 1];
        if (mostRecentSession.dateModified) {
          timestamp = mostRecentSession.dateModified;
        }
      }
      return new Date(timestamp);
    },
    set: function() {}
  },

  confidential: {
    get: function() {
      return this._confidential || FieldDBObject.DEFAULT_OBJECT;
    },
    set: function(value) {
      this.ensureSetViaAppropriateType("confidential", value);
      return this._confidential;
    }
  },

  publicCorpus: {
    get: function() {
      return this._publicCorpus || FieldDBObject.DEFAULT_STRING;
    },
    set: function(value) {
      if (value === this._publicCorpus) {
        return;
      }
      if (!value || (value !== "Public" && value !== "Private")) {
        this.warn("Corpora can be either Public or Private, if you make your corpus Public you can customize which fields are visible to public visitors.");
        value = "Private";
      }
      this._publicCorpus = value;
    }
  },

  /**
   * TODO decide if we want to fetch these from the server, and keep a fossil in the object?
   * @type {Object}
   */
  corpusMask: {
    get: function() {
      if (!this._corpusMask) {
        this.corpusMask = {
          "id": "corpus"
        };
        // this.corpusMask.fetch();
      }
      return this._corpusMask;
    },
    set: function(value) {
      this.ensureSetViaAppropriateType("corpusMask", value);
    }
  },

  corpus: {
    get: function() {
      return this;
    },
    set: function() {
      // do nothing
    }
  },

  publicSelf: {
    get: function() {
      console.error("publicSelf is deprecated, use corpusMask instead");
      return this.corpusMask;
    },
    set: function(value) {
      // console.error("publicSelf is deprecated, use corpusMask instead");
      this.corpusMask = value;
    }
  },

  validationStati: {
    get: function() {
      return this._validationStati || FieldDBObject.DEFAULT_COLLECTION;
    },
    set: function(value) {
      this.ensureSetViaAppropriateType("validationStati", value);
    }
  },

  tags: {
    get: function() {
      return this._tags || FieldDBObject.DEFAULT_COLLECTION;
    },
    set: function(value) {
      this.ensureSetViaAppropriateType("tags", value);
    }
  },

  fetch: {
    value: function(optionalUrl) {
      if (!this.id && this.dbname) {
        return this.loadCorpusByDBname(this.dbname);
      } else {
        if (optionalUrl) {
          this.warn("Using a custom url to fetch this Corpus." + optionalUrl);
        }
        return FieldDBObject.prototype.fetch.apply(this, arguments);
      }
    }
  },

  loadCorpusByDBname: {
    value: function(dbname) {
      if (!dbname) {
        throw new Error("Cannot load corpus, its dbname was undefined");
      }
      var deferred = this.loadCorpusByDBnameDeferred || Q.defer(),
        self = this;

      dbname = dbname.trim();

      this.dbname = dbname;
      this.loading = true;

      // this.debugMode = true;
      Q.nextTick(function() {

        var tryAgainInCaseThereWasALag = function(reason) {
          self.debug(reason);
          if (self.runningloadCorpusByDBname) {
            self.warn("Error finding a corpus in " + self.dbname + " database. This database will not function normally. Please report this.");
            self.bug("Error finding corpus details in " + self.dbname + " database. This database will not function normally. Please report this.");
            deferred.reject(reason);
            return;
          }
          self.runningloadCorpusByDBname = true;
          self.loadCorpusByDBnameDeferred = deferred;
          self.debug("Wating 1000ms to try to load again.");
          setTimeout(function() {
            self.loadCorpusByDBname(dbname);
          }, 1000);
        };

        self.fetchCollection(self.api).then(function(corpora) {
          self.debug(corpora);

          var corpusAsSelf = function(corpusid) {
            self.runningloadCorpusByDBname = false;
            delete self.loadCorpusByDBnameDeferred;
            self.id = corpusid;
            self.fetch().then(function(result) {
              self.debug("Finished fetch of corpus ", result);
              self.loading = false;
              deferred.resolve(result);
            }, function(reason) {
              self.loading = false;
              deferred.reject(reason);
            }).fail(function(error) {
              console.error(error.stack, self);
              deferred.reject(error);
            });
          };

          if (corpora.length === 1) {
            corpusAsSelf(corpora[0]._id);
          } else if (corpora.length > 1) {
            self.warn("Impossible to have more than one corpus for this dbname, marking irrelevant corpora as trashed");
            corpora.map(function(row) {
              if (row.value.dbname === self.dbname || row.value.pouchname === self.dbname) {
                corpusAsSelf(row.value._id);
              } else {
                self.warn("There were multiple corpora details in this database, it is probaly one of the old offline databases prior to v1.30 or the result of merged corpora. This is not really a problem, the correct details will be used, and this corpus details will be marked as deleted. " + row.value);
                row.value.trashed = "deleted";
                self.set(row.value).then(function(result) {
                  self.debug("flag as deleted succedded", result);
                }, function(reason) {
                  self.warn("flag as deleted failed", reason, row.value);
                }).fail(function(error) {
                  console.error(error.stack, self);
                  deferred.reject(error);
                });
              }
            });
          } else {
            tryAgainInCaseThereWasALag(corpora);
          }
        }, function(reason) {
          self.debug(JSON.stringify(reason));
          if (reason && reason.userFriendlyErrors && reason.userFriendlyErrors[0].indexOf("device will be unable to contact") > -1) {
            deferred.reject(reason);
          } else {
            tryAgainInCaseThereWasALag(reason);
          }

        });

      });

      return deferred.promise;
    }
  },

  fetchMask: {
    value: function() {
      this.todo("test fetchMask");
      if (!this.dbname) {
        throw new Error("Cannot load corpus's public self, its dbname was undefined");
      }
      var deferred = Q.defer(),
        self = this;

      Q.nextTick(function() {

        if (self.corpusMask && self.corpusMask.rev) {
          deferred.resolve(self.corpusMask);
          return;
        }

        self.corpusMask = new CorpusMask({
          dbname: self.dbname
        });

        self.corpusMask.fetch()
          .then(deferred.resolve, deferred.reject)
          .fail(function(error) {
            console.error(error.stack, self);
            deferred.reject(error);
          });

      });
      return deferred.promise;
    }
  },

  /**
   * backbone-couchdb adaptor set up
   */

  // 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: "private_corpora"
  },

  defaults: {
    get: function() {
      var corpusTemplate = JSON.parse(JSON.stringify(DEFAULT_CORPUS_MODEL));
      corpusTemplate.confidential.secretkey = FieldDBObject.uuidGenerator();
      return corpusTemplate;
    },
    set: function() {}
  },

  defaults_psycholinguistics: {
    get: function() {
      var doc = this.defaults;

      if (DEFAULT_PSYCHOLINGUISTICS_CORPUS_MODEL) {
        for (var property in DEFAULT_PSYCHOLINGUISTICS_CORPUS_MODEL) {
          if (DEFAULT_PSYCHOLINGUISTICS_CORPUS_MODEL.hasOwnProperty(property)) {
            doc[property] = DEFAULT_PSYCHOLINGUISTICS_CORPUS_MODEL[property];
          }
        }
        doc.participantFields = this.defaults.speakerFields.concat(doc.participantFields);
      }

      return JSON.parse(JSON.stringify(doc));
    },
    set: function() {}
  },

  /**
   * 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() {
      OPrime.bug("Sorry deleting corpora is not available right now. Too risky... ");
      if (true) {
        return;
      }
      /* TODO contact server to delte the corpus, if the success comes back, then do this */
      this.trashed = "deleted" + Date.now();
      this.save();
    }
  },

  /**
   *  This the function called by the add button, it adds a new comment state both to the collection and the model
   * @type {Object}
   */
  newComment: {
    value: function(commentstring) {
      var m = {
        "text": commentstring,
      };

      this.comments.add(m);
      this.unsavedChanges = true;

      window.app.addActivity({
        verb: "commented",
        verbicon: "icon-comment",
        directobjecticon: "",
        directobject: "'" + commentstring + "'",
        indirectobject: "on <i class='icon-cloud'></i><a href='#corpus/" + this.id + "'>this corpus</a>",
        teamOrPersonal: "team",
        context: " via Offline App."
      });

      window.app.addActivity({
        verb: "commented",
        verbicon: "icon-comment",
        directobjecticon: "",
        directobject: "'" + commentstring + "'",
        indirectobject: "on <i class='icon-cloud'></i><a href='#corpus/" + this.id + "'>" + this.get("title") + "</a>",
        teamOrPersonal: "personal",
        context: " via Offline App."
      });

      return m;
    }
  },

  currentSession: {
    get: function() {
      return this._currentSession;
    },
    set: function(value) {
      this._currentSession = value;
    }
  },

  /**
   * Builds a new session in this corpus, copying the current session's fields (if available) or the corpus' session fields.
   * @return {Session} a new session for this corpus
   */
  newSession: {
    value: function(options) {
      var sessionFields;
      if (this.currentSession && this.currentSession.sessionFields) {
        sessionFields = this.currentSession.sessionFields.clone();
      } else {
        sessionFields = this.sessionFields.clone();
      }
      var session = new Session({
        dbname: this.dbname,
        fields: sessionFields,
        confidential: this.confidential,
        // url: this.url
      });

      for (var field in options) {
        if (!options.hasOwnProperty(field)) {
          continue;
        }
        if (session.fields[field]) {
          this.debug("  this option appears to be a sessionField " + field);
          session.fields[field].value = options[field];
        } else {
          session[field] = options[field];
        }
      }

      return session;
    }
  },

  newDoc: {
    value: function(options) {
      return this.newDatum(options);
    }
  },

  newDatum: {
    value: function(options) {
      this.debug("Creating a datum for this corpus");
      if (!this.datumFields || !this.datumFields.clone) {
        throw new Error("This corpus has no default datum fields... It is unable to create a datum.");
      }
      var datum;
      if (options instanceof Corpus.DEFAULT_DATUM) {
        datum = options;
        datum.dbname = this.dbname;
        datum.confidential = this.confidential;
        datum = this.updateDatumToCorpusFields(datum);
      } else {
        datum = new Corpus.DEFAULT_DATUM({
          fields: new DatumFields(this.datumFields.cloneStructure()),
          dbname: this.dbname,
          confidential: this.confidential
        });
      }
      for (var field in options) {
        if (!options.hasOwnProperty(field)) {
          continue;
        }
        if (datum.fields[field]) {
          this.debug("  this option appears to be a datumField " + field);
          datum.fields[field].value = options[field];
        } else {
          datum[field] = options[field];
        }
      }
      datum.fossil = datum.toJSON();
      return datum;
    }
  },

  newDatumAsync: {
    value: function(options) {
      var deferred = Q.defer(),
        self = this;

      Q.nextTick(function() {
        var datum = self.newDatum(options);
        deferred.resolve(datum);
      });
      return deferred.promise;
    }
  },

  newField: {
    value: function(field) {
      field = field || {};

      if (!(field instanceof DatumField)) {
        field = new DatumField(field);
      }
      return field;
    }
  },

  addDatumField: {
    value: function(field) {
      if (!field.id && field.label) {
        field.id = field.label;
      }
      if (!(field instanceof DatumField)) {
        field = new DatumField(field);
      }
      this.datumFields.add(field);
    }
  },

  newSpeaker: {
    value: function(options) {
      var deferred = Q.defer(),
        self = this;

      Q.nextTick(function() {

        self.debug("Creating a datum for this corpus");
        if (!self.speakerFields || !self.speakerFields.clone) {
          throw new Error("This corpus has no default datum fields... It is unable to create a datum.");
        }
        var datum = new Speaker({
          speakerFields: new DatumFields(self.speakerFields.clone()),
          confidential: self.confidential
        });
        for (var field in options) {
          if (!options.hasOwnProperty(field)) {
            continue;
          }
          if (datum.speakerFields[field]) {
            self.debug("  this option appears to be a datumField " + field);
            datum.speakerFields[field].value = options[field];
          } else {
            datum[field] = options[field];
          }
        }
        deferred.resolve(datum);
      });
      return deferred.promise;
    }
  },

  updateDatumToCorpusFields: {
    value: function(datum) {
      if (!this.datumFields) {
        return datum;
      }
      if (!datum.fields) {
        datum.fields = this.datumFields.clone();
        return datum;
      }
      datum.fields = new DatumFields().merge(this.datumFields, datum.fields);
      return datum;
    }
  },

  updateSpeakerToCorpusFields: {
    value: function(speaker) {
      if (!this.speakerFields) {
        this.speakerFields = this.defaults_psycholinguistics.speakerFields;
      }
      if (!speaker.fields) {
        speaker.fields = this.speakerFields.clone();
        return speaker;
      }
      speaker.fields = new DatumFields().merge(this.speakerFields, speaker.fields);
      return speaker;
    }
  },

  updateParticipantToCorpusFields: {
    value: function(participant) {
      if (!this.participantFields) {
        this.participantFields = this.defaults_psycholinguistics.participantFields;
      }
      if (!participant.fields) {
        participant.fields = this.participantFields.clone();
        return participant;
      }
      participant.fields = new DatumFields().merge(this.participantFields, participant.fields, "overwrite");
      return participant;
    }
  },

  /**
   * Builds a new corpus based on this one (if this is not the team's practice corpus)
   * @return {Corpus} a new corpus based on this one
   */
  newCorpus: {
    value: function(options) {
      var corpus,
        self = this;

      if (this.dbname && this.dbname.indexOf("firstcorpus") > -1) {
        corpus = new Corpus(Corpus.prototype.defaults);
      } else {
        corpus = this.clone();

        corpus.comments = [];
        corpus.confidential = new Confidential().fillWithDefaults();

        var fieldsToClear = ["datumFields", "sessionFields", "conversationFields", "participantFields", "speakerFields", "fields"];
        //clear out search terms from the new corpus's datum fields
        var defaults = this.defaults;
        fieldsToClear.map(function(fieldsType) {
          if (self[fieldsType]) {
            self.debug("Cloning structure only of fieldsType: ", fieldsType);
            corpus[fieldsType] = self[fieldsType].cloneStructure();
          } else {
            self.debug("fieldsType " + fieldsType + " was missing on this corpus, it's copy will have the fields. ", self);
            corpus[fieldsType] = defaults[fieldsType];
          }
        });

        if (this.dbname) {
          corpus.dbname = this.dbname + "_copy";
        }
        corpus.title = corpus.title + " copy";
        corpus.titleAsUrl = corpus.titleAsUrl + "Copy";
        corpus.description = "Copy of: " + corpus.description;
      }

      for (var aproperty in options) {
        if (options.hasOwnProperty(aproperty)) {
          corpus[aproperty] = options[aproperty];
        }
      }

      return corpus;
    }
  },

  cloneStructure: {
    value: function() {
      return this.newCorpus();
    }
  },

  /**
   * DO NOT store in attributes when saving to pouch (too big)
   * @type {FieldDBGlosser}
   */
  glosser: {
    get: function() {
      return this.glosserExternalObject;
    },
    set: function(value) {
      if (value === this.glosserExternalObject) {
        return;
      }
      this.glosserExternalObject = value;
    }
  },

  lexicon: {
    get: function() {
      return this.lexiconExternalObject;
    },
    set: function(value) {
      if (value === this.lexiconExternalObject) {
        return;
      }
      this.lexiconExternalObject = value;
    }
  },

  find: {
    value: function(uri) {
      var deferred = Q.defer();

      if (!uri) {
        throw new Error("Uri must be specified ");
      }

      Q.nextTick(function() {
        deferred.resolve([]); /* TODO try fetching this uri */
      });

      return deferred.promise;
    }
  },

  /**
   *  This function looks for the field's details from the corpus fields, if it exists it returns that field template.
   *
   * If the field isnt in the corpus' fields exactly, it looks for fields which this field should map to (eg, if the field is codepermanent it can be mapped to anonymouscode)
   * @param  {String/Object} field A datumField to look for, or the label/id of a datum field to look for.
   * @return {DatumField}       A datum field with details filled in from the corresponding field in the corpus, or from a template.
   */
  normalizeFieldWithExistingCorpusFields: {
    value: function(field, optionalAllFields) {
      if (field && typeof field.trim === "function") {
        field = field.trim();
      }
      if (field === undefined || field === null || field === "") {
        return;
      }
      if (typeof field !== "object") {
        field = {
          id: field
        };
      }
      var incomingFieldIdOrLabel = field.id || field.label;
      // incomingFieldIdOrLabel = incomingFieldIdOrLabel + "";
      if (incomingFieldIdOrLabel === undefined || incomingFieldIdOrLabel === null || incomingFieldIdOrLabel === "") {
        return;
      }
      // this.debugMode = true;
      // this.debug("Normalizing " + incomingFieldIdOrLabel + " if it is known to this corpus.");
      var fuzzyLabel = incomingFieldIdOrLabel.toLowerCase().replace(/[^a-z]/g, "");
      if (!optionalAllFields) {
        optionalAllFields = new DatumFields();
        if (this.datumFields && this.datumFields.length > 0) {
          optionalAllFields.add(this.datumFields.toJSON());
        } else {
          optionalAllFields.add(DEFAULT_CORPUS_MODEL.datumFields);
        }
        if (this.participantFields && this.participantFields.length > 0 && this.participantFields.toJSON) {
          optionalAllFields.add(this.participantFields.toJSON());
        } else {
          optionalAllFields.add(DEFAULT_PSYCHOLINGUISTICS_CORPUS_MODEL.participantFields);
        }
        if (this.speakerFields && this.speakerFields.length > 0 && this.speakerFields.toJSON) {
          optionalAllFields.add(this.speakerFields.toJSON());
        } else {
          optionalAllFields.add(DEFAULT_CORPUS_MODEL.speakerFields);
        }
        if (this.conversationFields && this.conversationFields.length > 0 && this.conversationFields.toJSON) {
          optionalAllFields.add(this.conversationFields.toJSON());
        } else {
          optionalAllFields.add(DEFAULT_CORPUS_MODEL.conversationFields);
        }
        this.debug("Using a clone of the corpus fields. ", optionalAllFields);
      }
      var correspondingDatumField = optionalAllFields.find(field, null, true);
      /* if there is no corresponding field yet in the optionalAllFields, then maybe there is a field which is normalized to this label */
      if (!correspondingDatumField || correspondingDatumField.length === 0) {
        if (fuzzyLabel.indexOf("checkedwith") > -1 || fuzzyLabel.indexOf("checkedby") > -1 || fuzzyLabel.indexOf("publishedin") > -1) {
          correspondingDatumField = optionalAllFields.find("validationStatus");
          if (correspondingDatumField.length > 0) {
            this.debug("This header matches an existing corpus field. ", correspondingDatumField);
            correspondingDatumField[0].labelFieldLinguists = field.labelFieldLinguists || incomingFieldIdOrLabel;
            correspondingDatumField[0].labelExperimenters = field.labelExperimenters || incomingFieldIdOrLabel;
          }
        } else if (fuzzyLabel.indexOf("codepermanent") > -1) {
          correspondingDatumField = optionalAllFields.find("anonymouscode");
          if (correspondingDatumField.length > 0) {
            this.debug("This header matches an existing corpus field. ", correspondingDatumField);
            correspondingDatumField[0].labelFieldLinguists = field.labelFieldLinguists || incomingFieldIdOrLabel;
            correspondingDatumField[0].labelExperimenters = field.labelExperimenters || incomingFieldIdOrLabel;
          }
        } else if (fuzzyLabel.indexOf("nsection") > -1) {
          correspondingDatumField = optionalAllFields.find("courseNumber");
          if (correspondingDatumField.length > 0) {
            this.debug("This header matches an existing corpus field. ", correspondingDatumField);
            correspondingDatumField[0].labelFieldLinguists = field.labelFieldLinguists || incomingFieldIdOrLabel;
            correspondingDatumField[0].labelExperimenters = field.labelExperimenters || incomingFieldIdOrLabel;
          }
        } else if (fuzzyLabel.indexOf("prenom") > -1 || fuzzyLabel.indexOf("prnom") > -1) {
          correspondingDatumField = optionalAllFields.find("firstname");
          if (correspondingDatumField.length > 0) {
            this.debug("This header matches an existing corpus field. ", correspondingDatumField);
            correspondingDatumField[0].labelFieldLinguists = field.labelFieldLinguists || incomingFieldIdOrLabel;
            correspondingDatumField[0].labelExperimenters = field.labelExperimenters || incomingFieldIdOrLabel;
          }
        } else if (fuzzyLabel.indexOf("nomdefamille") > -1) {
          correspondingDatumField = optionalAllFields.find("lastname");
          if (correspondingDatumField.length > 0) {
            this.debug("This header matches an existing corpus field. ", correspondingDatumField);
            correspondingDatumField[0].labelFieldLinguists = field.labelFieldLinguists || incomingFieldIdOrLabel;
            correspondingDatumField[0].labelExperimenters = field.labelExperimenters || incomingFieldIdOrLabel;
          }
        } else if (fuzzyLabel.indexOf("datedenaissance") > -1) {
          correspondingDatumField = optionalAllFields.find("dateofbirth");
          if (correspondingDatumField.length > 0) {
            this.debug("This header matches an existing corpus field. ", correspondingDatumField);
            correspondingDatumField[0].labelFieldLinguists = field.labelFieldLinguists || incomingFieldIdOrLabel;
            correspondingDatumField[0].labelExperimenters = field.labelExperimenters || incomingFieldIdOrLabel;
          }
        }
      }

      /* if the field is still not defined inthe corpus, construct a blank field with this label */
      if (!correspondingDatumField || correspondingDatumField.length === 0) {
        correspondingDatumField = [new DatumField(DatumField.prototype.defaults)];
        correspondingDatumField[0].id = incomingFieldIdOrLabel;
        correspondingDatumField[0].labelFieldLinguists = incomingFieldIdOrLabel;
        // correspondingDatumField[0].notInCorpus = true;
        optionalAllFields.add(correspondingDatumField[0]);
      }
      if (correspondingDatumField && correspondingDatumField[0]) {
        correspondingDatumField = correspondingDatumField[0];
      }

      this.debug("correspondingDatumField ", correspondingDatumField);

      if (correspondingDatumField instanceof DatumField) {
        return correspondingDatumField;
      } else {
        return new DatumField(correspondingDatumField);
      }
    }
  },

  prepareANewOfflinePouch: {
    value: function() {
      throw new Error("I dont know how to prepareANewOfflinePouch");
    }
  },

  /**
   * 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 corpus to the corpus if it is in the right corpus, and wasn't already there
   * - Adds the corpus to the user if it wasn't already there
   * - Adds an activity to the logged in user with diff in what the user changed.
   * @return {Promise} promise for the saved corpus
   */
  saveCorpus: {
    value: function() {
      var deferred = Q.defer(),
        self = this;

      var newModel = false;
      if (!this.id) {
        self.debug("New corpus");
        newModel = true;
      } else {
        self.debug("Existing corpus");
      }
      var oldrev = this.get("_rev");

      this.timestamp = Date.now();

      self.unsavedChanges = false;
      self.save().then(function(model) {
        var title = model.title;
        var differences = "#diff/oldrev/" + oldrev + "/newrev/" + model._rev;
        var verb = "modified";
        var verbicon = "icon-pencil";
        if (newModel) {
          verb = "added";
          verbicon = "icon-plus";
        }
        var teamid = self.dbname.split("-")[0];
        window.app.addActivity({
          verb: "<a href='" + differences + "'>" + verb + "</a> ",
          verbmask: verb,
          verbicon: verbicon,
          directobject: "<a href='#corpus/" + model.id + "'>" + title + "</a>",
          directobjectmask: "a corpus",
          directobjecticon: "icon-cloud",
          indirectobject: "created by <a href='#user/" + teamid + "'>" + teamid + "</a>",
          context: " via Offline App.",
          contextmask: "",
          teamOrPersonal: "personal"
        });
        window.app.addActivity({
          verb: "<a href='" + differences + "'>" + verb + "</a> ",
          verbmask: verb,
          verbicon: verbicon,
          directobject: "<a href='#corpus/" + model.id + "'>" + title + "</a>",
          directobjectmask: "a corpus",
          directobjecticon: "icon-cloud",
          indirectobject: "created by <a href='#user/" + teamid + "'>this team</a>",
          context: " via Offline App.",
          contextmask: "",
          teamOrPersonal: "team"
        });
        deferred.resolve(self);
      }, deferred.reject).fail(
        function(error) {
          console.error(error.stack, self);
          deferred.reject(error);
        });

      return deferred.promise;
    }
  },

  /**
   * If more views are added to corpora, add them here
   * @returns {} an object containing valid map reduce functions
   * TODO: add conversation search to the get_datum_fields function
   */
  validDBQueries: {
    value: function() {
      return {
        // activities: {
        //   url: "/_design/deprecated/_view/activities",
        //   map: requireoff("./../../map_reduce_unused/views/activities/map")
        // },
        // add_synctactic_category: {
        //   url: "/_design/deprecated/_view/add_synctactic_category",
        //   map: requireoff("./../../map_reduce_unused/views/add_synctactic_category/map")
        // },
        // audioIntervals: {
        //   url: "/_design/deprecated/_view/audioIntervals",
        //   map: requireoff("./../../map_reduce_unused/views/audioIntervals/map")
        // },
        // byCollection: {
        //   url: "/_design/deprecated/_view/byCollection",
        //   map: requireoff("./../../map_reduce_unused/views/byCollection/map")
        // },
        // by_date: {
        //   url: "/_design/deprecated/_view/by_date",
        //   map: requireoff("./../../map_reduce_unused/views/by_date/map")
        // },
        // by_rhyming: {
        //   url: "/_design/deprecated/_view/by_rhyming",
        //   map: requireoff("./../../map_reduce_unused/views/by_rhyming/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/by_rhyming/reduce")
        // },
        // cleaning_example: {
        //   url: "/_design/deprecated/_view/cleaning_example",
        //   map: requireoff("./../../map_reduce_unused/views/cleaning_example/map")
        // },
        // corpora: {
        //   url: "/_design/deprecated/_view/corpora",
        //   map: requireoff("./../../map_reduce_unused/views/corpora/map")
        // },
        // datalists: {
        //   url: "/_design/deprecated/_view/datalists",
        //   map: requireoff("./../../map_reduce_unused/views/datalists/map")
        // },
        // datums: {
        //   url: "/_design/deprecated/_view/datums",
        //   map: requireoff("./../../map_reduce_unused/views/datums/map")
        // },
        // datums_by_user: {
        //   url: "/_design/deprecated/_view/datums_by_user",
        //   map: requireoff("./../../map_reduce_unused/views/datums_by_user/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/datums_by_user/reduce")
        // },
        // datums_chronological: {
        //   url: "/_design/deprecated/_view/datums_chronological",
        //   map: requireoff("./../../map_reduce_unused/views/datums_chronological/map")
        // },
        // deleted: {
        //   url: "/_design/deprecated/_view/deleted",
        //   map: requireoff("./../../map_reduce_unused/views/deleted/map")
        // },
        // export_eopas_xml: {
        //   url: "/_design/deprecated/_view/export_eopas_xml",
        //   map: requireoff("./../../map_reduce_unused/views/export_eopas_xml/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/export_eopas_xml/reduce")
        // },
        // get_corpus_datum_tags: {
        //   url: "/_design/deprecated/_view/get_corpus_datum_tags",
        //   map: requireoff("./../../map_reduce_unused/views/get_corpus_datum_tags/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/get_corpus_datum_tags/reduce")
        // },
        // get_corpus_fields: {
        //   url: "/_design/deprecated/_view/get_corpus_fields",
        //   map: requireoff("./../../map_reduce_unused/views/get_corpus_fields/map")
        // },
        // get_corpus_validationStati: {
        //   url: "/_design/deprecated/_view/get_corpus_validationStati",
        //   map: requireoff("./../../map_reduce_unused/views/get_corpus_validationStati/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/get_corpus_validationStati/reduce")
        // },
        // get_datum_fields: {
        //   url: "/_design/deprecated/_view/get_datum_fields",
        //   map: requireoff("./../../map_reduce_unused/views/get_datum_fields/map")
        // },
        // get_datums_by_session_id: {
        //   url: "/_design/deprecated/_view/get_datums_by_session_id",
        //   map: requireoff("./../../map_reduce_unused/views/get_datums_by_session_id/map")
        // },
        // get_frequent_fields: {
        //   url: "/_design/deprecated/_view/get_frequent_fields",
        //   map: requireoff("./../../map_reduce_unused/views/get_frequent_fields/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/get_frequent_fields/reduce")
        // },
        // get_search_fields_chronological: {
        //   url: "/_design/deprecated/_view/get_search_fields_chronological",
        //   map: requireoff("./../../map_reduce_unused/views/get_search_fields_chronological/map")
        // },
        // glosses_in_utterance: {
        //   url: "/_design/deprecated/_view/glosses_in_utterance",
        //   map: requireoff("./../../map_reduce_unused/views/glosses_in_utterance/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/glosses_in_utterance/reduce")
        // },
        // lexicon_create_tuples: {
        //   url: "/_design/deprecated/_view/lexicon_create_tuples",
        //   map: requireoff("./../../map_reduce_unused/views/lexicon_create_tuples/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/lexicon_create_tuples/reduce")
        // },
        // morpheme_neighbors: {
        //   url: "/_design/deprecated/_view/morpheme_neighbors",
        //   map: requireoff("./../../map_reduce_unused/views/morpheme_neighbors/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/morpheme_neighbors/reduce")
        // },
        // morphemes_in_gloss: {
        //   url: "/_design/deprecated/_view/morphemes_in_gloss",
        //   map: requireoff("./../../map_reduce_unused/views/morphemes_in_gloss/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/morphemes_in_gloss/reduce")
        // },
        // recent_comments: {
        //   url: "/_design/deprecated/_view/recent_comments",
        //   map: requireoff("./../../map_reduce_unused/views/recent_comments/map")
        // },
        // sessions: {
        //   url: "/_design/deprecated/_view/sessions",
        //   map: requireoff("./../../map_reduce_unused/views/sessions/map")
        // },
        // users: {
        //   url: "/_design/deprecated/_view/users",
        //   map: requireoff("./../../map_reduce_unused/views/users/map")
        // },
        // word_list: {
        //   url: "/_design/deprecated/_view/word_list",
        //   map: requireoff("./../../map_reduce_unused/views/word_list/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/word_list/reduce")
        // },
        // map_reduce_unused_word_list_rdf: {
        //   url: "/_design/deprecated/_view/map_reduce_unused_word_list_rdf",
        //   map: requireoff("./../../map_reduce_unused/views/word_list_rdf/map"),
        //   reduce: requireoff("./../../map_reduce_unused/views/word_list_rdf/reduce")
        // }
      };
    }
  },

  validate: {
    value: function(attrs) {
      attrs = attrs || this;
      if (attrs.publicCorpus) {
        if (attrs.publicCorpus !== "Public") {
          if (attrs.publicCorpus !== "Private") {
            return "Corpus must be either Public or Private"; //TODO test this.
          }
        }
      }
    }
  },

  /**
   * This function takes in a dbname, which could be different
   * from the current corpus in case there is a master corpus with
   * more/better monolingual data.
   *
   * @param dbname
   * @param callback
   */
  buildMorphologicalAnalyzerFromTeamServer: {
    value: function(dbname, callback) {
      if (!dbname) {
        dbname = this.dbname;
      }
      this.glosser.downloadPrecedenceRules(dbname, this.glosserURL, callback);
    }
  },
  /**
   * This function takes in a dbname, which could be different
   * from the current corpus incase there is a master corpus wiht
   * more/better monolingual data.
   *
   * @param dbname
   * @param callback
   */
  buildLexiconFromTeamServer: {
    value: function(dbname, callback) {
      if (!dbname) {
        dbname = this.dbname;
      }
      this.lexicon.buildLexiconFromCouch(dbname, callback);
    }
  },

  /**
   * This function takes in a dbname, which could be different
   * from the current corpus incase there is a master corpus wiht
   * more representative datum
   * example : https://corpusdev.example.org/lingllama-cherokee/_design/deprecated/_view/get_frequent_fields?group=true
   *
   * It takes the values stored in the corpus, if set, otherwise it will take the values from this corpus since the window was last refreshed
   *
   * If a url is passed, it contacts the server for fresh info.
   *
   * @param dbname
   * @param callback
   */
  getFrequentDatumFields: {
    value: function() {
      return this.getFrequentValues("fields", ["judgement", "utterance", "morphemes", "gloss", "translation"]);
    }
  },

  /**
   * This function takes in a dbname, which could be different
   * from the current corpus incase there is a master corpus wiht
   * more representative datum
   * example : https://corpusdev.example.org/lingllama-cherokee/_design/deprecated/_view/get_corpus_validationStati?group=true
   *
   * It takes the values stored in the corpus, if set, otherwise it will take the values from this corpus since the window was last refreshed
   *
   * If a url is passed, it contacts the server for fresh info.
   *
   * @param dbname
   * @param callback
   */
  getFrequentDatumValidationStates: {
    value: function() {
      return this.getFrequentValues("validationStatus", ["Checked", "Deleted", "ToBeCheckedByAnna", "ToBeCheckedByBill", "ToBeCheckedByClaude"]);
    }
  },

  getCorpusSpecificLocalizations: {
    value: function(optionalLocaleCode) {
      var self = this;

      if (optionalLocaleCode) {
        this.todo("Test the loading of an optionalLocaleCode");
        this.get(optionalLocaleCode + "/messages.json").then(function(locale) {
          if (!locale) {
            self.warn("the requested locale was empty.");
            return;
          }
          self.application.contextualizer.addMessagesToContextualizedStrings("null", locale);
        }, function(error) {
          self.warn("The requested locale wasn't loaded");
          self.debug("locale loading error", error);
        }).fail(function(error) {
          console.error(error.stack, self);
        });
      } else {
        this.fetchCollection("locales").then(function(locales) {
          for (var localeIndex = 0; localeIndex < locales.length; localeIndex++) {
            if (!locales[localeIndex]) {
              self.warn("the requested locale was empty.");
              continue;
            }
            self.application.contextualizer.addMessagesToContextualizedStrings(null, locales[localeIndex]);
          }
        }, function(error) {
          self.warn("The locales didn't loaded");
          self.debug("locale loading error", error);
        }).fail(function(error) {
          console.error(error.stack, self);
        });
      }

      return this;
    }
  },

  getFrequentValues: {
    value: function(fieldname, defaults) {
      var deferred = Q.defer(),
        self;

      if (!defaults) {
        defaults = self["defaultFrequentDatum" + fieldname];
      }

      /* if we have already asked the server in this page load, return */
      if (self["frequentDatum" + fieldname]) {
        Q.nextTick(function() {
          deferred.resolve(self["frequentDatum" + fieldname]);
        });
        return deferred.promise;
      }

      // var jsonUrl = self.validDBQueries["get_corpus_" + fieldname].url + "?group=true&limit=100";
      this.fetchCollection("frequentDatum" + fieldname, 0, 0, 100, true).then(function(frequentValues) {
        /*
         * TODO Hide optionally specified values
         */
        self["frequentDatum" + fieldname] = frequentValues;
        deferred.resolve(frequentValues);
      }, function(response) {
        self.debug("resolving defaults for frequentDatum" + fieldname, response);
        deferred.resolve(defaults);
      });

      return deferred.promise;
    }
  },
  /**
   * This function takes in a dbname, which could be different
   * from the current corpus incase there is a master corpus wiht
   * more representative datum
   * example : https://corpusdev.example.org/lingllama-cherokee/_design/deprecated/_view/get_corpus_validationStati?group=true
   *
   * It takes the values stored in the corpus, if set, otherwise it will take the values from this corpus since the window was last refreshed
   *
   * If a url is passed, it contacts the server for fresh info.
   *
   * @param dbname
   * @param callback
   */
  getFrequentDatumTags: {
    value: function() {
      return this.getFrequentValues("tags", ["Passive", "WH", "Indefinte", "Generic", "Agent-y", "Causative", "Pro-drop", "Ambigous"]);
    }
  },
  changeCorpusPublicPrivate: {
    value: function() {
      //      alert("TODO contact server to change the public private of the corpus");
      throw new Error(" I dont know how change this corpus' public/private setting ");
    }
  },

  toJSON: {
    value: function(includeEvenEmptyAttributes, removeEmptyAttributes) {
      this.debug("Customizing toJSON ", includeEvenEmptyAttributes, removeEmptyAttributes);
      var attributesNotToJsonify = ["gravatar", "OLAC_export_connections", "url"];
      var json = FieldDBObject.prototype.toJSON.apply(this, [includeEvenEmptyAttributes, removeEmptyAttributes, attributesNotToJsonify]);

      if (!json) {
        this.warn("Not returning json right now.");
        return;
      }
      if (this.team && typeof this.team.toJSON === "function") {
        json.team = this.team.toJSON();
      }
      if (this.confidential && typeof this.confidential.toJSON === "function") {
        json.confidential = this.confidential.toJSON();
      }
      if (this.activityConnection && typeof this.activityConnection.toJSON === "function") {
        json.activityConnection = this.activityConnection.toJSON();
      }

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

});

exports.Corpus = Corpus;
exports.FieldDatabase = Corpus;