<commit_before>define([
    "use!backbone",
    "audio_video/AudioVideo",
    "comment/Comments",
    "datum/DatumField",
    "datum/DatumFields",
    "datum/DatumState",
    "datum/DatumStates",
    "datum/DatumTag",
    "datum/DatumTags",
    "datum/Session",
    "libs/Utils"
], function(
    Backbone,
    AudioVideo,
    Comments,
    DatumField,
    DatumFields,
    DatumState,
    DatumStates,
    DatumTag,
    DatumTags,
    Session
) {
  var Datum = Backbone.Model.extend(
  /** @lends Datum.prototype */
  {
    /**
     * @class The Datum widget is the place where all linguistic data is
     *        entered; one at a time.
     *
     * @property {DatumField} utterance The utterance field generally
     *           corresponds to the first line in linguistic examples that can
     *           either be written in the language's orthography or a
     *           romanization of the language. An additional field can be added
     *           if the language has a non-roman script.
     * @property {DatumField} gloss The gloss field corresponds to the gloss
     *           line in linguistic examples where the morphological details of
     *           the words are displayed.
     * @property {DatumField} translation The translation field corresponds to
     *           the third line in linguistic examples where in general an
     *           English translation. An additional field can be added if
     *           translations into other languages is needed.
     * @property {DatumField} judgment The judgment is the grammaticality
     *           judgment associated with the datum, so grammatical,
     *           ungrammatical, felicitous, unfelicitous etc.
     * @property {DatumState} state When a datum is created, it can be tagged
     *           with a state, such as 'to be checked with an consultant'.
     * @property {AudioVisual} audioVisual 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.
     *
     * @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.
     *
     * @extends Backbone.Model
     * @constructs
     */
    initialize : function() {
      if(typeof this.get("audioVideo") == "function"){
        this.set("audioVideo",new AudioVideo());
      }
    },

    relativizePouchToACorpus : function(corpus){
      //rebuild the pouch and touchdb urls to be relative to the active corpus TODO users shouldnt get saved in their corpus or should they? if they are saved, then if you replcate the corpus you can eaisly see the collaborators/contributors profiles since they are in the corpus. but they might be out of date.
      var c = corpus.get("couchConnection");
      this.pouch = Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl+c.corpusname
          : Utils.pouchUrl+c.corpusname);
    },

    defaults : {
      datumFields : new DatumFields(),
      audioVideo : new AudioVideo(),
      comments : new Comments(),
      datumState : new DatumState(),
      datumState : new DatumState(),      // The selected DatumState
      datumTags : new DatumTags(),
      dateEntered : new Date()
    },

    model : {
      datumFields : DatumFields,
      audioVideo : AudioVideo,
      session : Session,
      comments : Comments,
      datumState : DatumState,
      datumState : DatumState,      // The selected DatumState
      datumTags : DatumTags
    },

    parse : function(response) {
      if (response.ok === undefined) {
        for (var key in this.model) {
          var embeddedClass = this.model[key];
          var embeddedData = response[key];
          response[key] = new embeddedClass(embeddedData, {parse:true});
        }
      }

      return response;
    },

    pouch : Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl
        : Utils.pouchUrl),

    searchByQueryString : function(queryString, callback) {
      var self = this;
      this.pouch(function(err, db) {
        // Code for get_datum_field/get_datum_fields
        //
        // function(doc) {
        //   if ((doc.datumFields) && (doc.session)) {
        //     var obj = {};
        //     for (i = 0; i < doc.datumFields.length; i++) {
        //       if (doc.datumFields[i].value) {
        //         obj[doc.datumFields[i].label] = doc.datumFields[i].value;
        //       }
        //     }
        //     if (doc.session.sessionFields) {
        //       for (j = 0; j < doc.session.sessionFields.length; j++) {
        //         if (doc.session.sessionFields[j].value) {
        //           obj[doc.session.sessionFields[j].label] = doc.doc.session.sessionFields[j].value;
        //         }
        //       }
        //     }
        //     emit(obj, doc._id);
        //   }
        // }
        db.query("get_datum_field/get_datum_fields", {reduce: false}, function(err, response) {
          var matchIds = [];

          if (!err) {
            // Process the given query string into tokens
            var queryTokens = self.processQueryString(queryString);

            // Go through all the rows of results
            for (i in response.rows) {
              // Determine if this datum matches the first search criteria
              var thisDatumIsIn = self.matchesSingleCriteria(response.rows[i].key, queryTokens[0]);

              // Progressively determine whether the datum still matches based on
              // subsequent search criteria
              for (j = 1; j < queryTokens.length; j += 2) {
                if (queryTokens[j] == "AND") {
                  // Short circuit: if it's already false then it continues to be false
                  if (!thisDatumIsIn) {
                    break;
                  }

                  // Do an intersection
                  thisDatumIsIn = thisDatumIsIn && self.matchesSingleCriteria(response.rows[i].key, queryTokens[j+1]);
                } else {
                  // Do a union
                  thisDatumIsIn = thisDatumIsIn || self.matchesSingleCriteria(response.rows[i].key, queryTokens[j+1]);
                }
              }

              // 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);
              }
            }
          }

          callback(matchIds);
        });
      });
    },

    /**
     * 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 : function(objectToSearchThrough, criteria) {
      var delimiterIndex = criteria.indexOf(":");
      var label = criteria.substring(0, delimiterIndex);
      var value = criteria.substring(delimiterIndex + 1);

      return objectToSearchThrough[label] && (objectToSearchThrough[label].toLowerCase().indexOf(value) >= 0);
    },

    /**
     * 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 : function(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 (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) {
          currentString = currentString + " " + currentItem.toLowerCase();
        } else {
          currentString = currentItem.toLowerCase();
        }
      }
      queryTokens.push(currentString);

      return queryTokens;
    },

    /**
     * Clone the current Datum and return the clone. The clone is put in the current
     * Session, regardless of the origin Datum's Session.
     *
     * @return The clone of the current Datum.
     */
    clone : function() {
      // Create a new Datum based on the current Datum
      var datum = new Datum({
        audioVideo : new AudioVideo(this.get("audioVideo").toJSON(), {parse: true}),
        comments : new Comments(this.get("comments").toJSON(), {parse: true}),
        dateEntered : this.get("dateEntered"),
        datumFields : new DatumFields(this.get("datumFields").toJSON(), {parse: true}),
        datumState : new DatumState(this.get("datumState").toJSON(), {parse: true}),
        datumStates : new DatumStates(this.get("datumStates").toJSON(), {parse: true}),
        datumTags : new DatumTags(this.get("datumTags").toJSON(), {parse: true})
        // Don't need to do Session here since it will be overwritten in DatumContainerEditView.prependDatum()
      });

      return datum;
    }
  });

  return Datum;
});
<commit_msg>Bug fix: datumState was being mentioned twice, when one of them should...<commit_after>define([
    "use!backbone",
    "audio_video/AudioVideo",
    "comment/Comments",
    "datum/DatumField",
    "datum/DatumFields",
    "datum/DatumState",
    "datum/DatumStates",
    "datum/DatumTag",
    "datum/DatumTags",
    "datum/Session",
    "libs/Utils"
], function(
    Backbone,
    AudioVideo,
    Comments,
    DatumField,
    DatumFields,
    DatumState,
    DatumStates,
    DatumTag,
    DatumTags,
    Session
) {
  var Datum = Backbone.Model.extend(
  /** @lends Datum.prototype */
  {
    /**
     * @class The Datum widget is the place where all linguistic data is
     *        entered; one at a time.
     *
     * @property {DatumField} utterance The utterance field generally
     *           corresponds to the first line in linguistic examples that can
     *           either be written in the language's orthography or a
     *           romanization of the language. An additional field can be added
     *           if the language has a non-roman script.
     * @property {DatumField} gloss The gloss field corresponds to the gloss
     *           line in linguistic examples where the morphological details of
     *           the words are displayed.
     * @property {DatumField} translation The translation field corresponds to
     *           the third line in linguistic examples where in general an
     *           English translation. An additional field can be added if
     *           translations into other languages is needed.
     * @property {DatumField} judgment The judgment is the grammaticality
     *           judgment associated with the datum, so grammatical,
     *           ungrammatical, felicitous, unfelicitous etc.
     * @property {DatumState} state When a datum is created, it can be tagged
     *           with a state, such as 'to be checked with an consultant'.
     * @property {AudioVisual} audioVisual 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.
     *
     * @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.
     *
     * @extends Backbone.Model
     * @constructs
     */
    initialize : function() {
      if(typeof this.get("audioVideo") == "function"){
        this.set("audioVideo",new AudioVideo());
      }
    },

    relativizePouchToACorpus : function(corpus){
      //rebuild the pouch and touchdb urls to be relative to the active corpus TODO users shouldnt get saved in their corpus or should they? if they are saved, then if you replcate the corpus you can eaisly see the collaborators/contributors profiles since they are in the corpus. but they might be out of date.
      var c = corpus.get("couchConnection");
      this.pouch = Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl+c.corpusname
          : Utils.pouchUrl+c.corpusname);
    },

    defaults : {
      audioVideo : new AudioVideo(),
      comments : new Comments(),
      datumState : new DatumState(),      // The selected DatumState
      datumTags : new DatumTags(),
      dateEntered : new Date()
    },

    model : {
      datumFields : DatumFields,
      audioVideo : AudioVideo,
      session : Session,
      comments : Comments,
      datumStates : DatumStates,
      datumState : DatumState,      // The selected DatumState
      datumTags : DatumTags
    },

    parse : function(response) {
      if (response.ok === undefined) {
        for (var key in this.model) {
          var embeddedClass = this.model[key];
          var embeddedData = response[key];
          response[key] = new embeddedClass(embeddedData, {parse:true});
        }
      }

      return response;
    },

    pouch : Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl
        : Utils.pouchUrl),

    searchByQueryString : function(queryString, callback) {
      var self = this;
      this.pouch(function(err, db) {
        // Code for get_datum_field/get_datum_fields
        //
        // function(doc) {
        //   if ((doc.datumFields) && (doc.session)) {
        //     var obj = {};
        //     for (i = 0; i < doc.datumFields.length; i++) {
        //       if (doc.datumFields[i].value) {
        //         obj[doc.datumFields[i].label] = doc.datumFields[i].value;
        //       }
        //     }
        //     if (doc.session.sessionFields) {
        //       for (j = 0; j < doc.session.sessionFields.length; j++) {
        //         if (doc.session.sessionFields[j].value) {
        //           obj[doc.session.sessionFields[j].label] = doc.doc.session.sessionFields[j].value;
        //         }
        //       }
        //     }
        //     emit(obj, doc._id);
        //   }
        // }
        db.query("get_datum_field/get_datum_fields", {reduce: false}, function(err, response) {
          var matchIds = [];

          if (!err) {
            // Process the given query string into tokens
            var queryTokens = self.processQueryString(queryString);

            // Go through all the rows of results
            for (i in response.rows) {
              // Determine if this datum matches the first search criteria
              var thisDatumIsIn = self.matchesSingleCriteria(response.rows[i].key, queryTokens[0]);

              // Progressively determine whether the datum still matches based on
              // subsequent search criteria
              for (j = 1; j < queryTokens.length; j += 2) {
                if (queryTokens[j] == "AND") {
                  // Short circuit: if it's already false then it continues to be false
                  if (!thisDatumIsIn) {
                    break;
                  }

                  // Do an intersection
                  thisDatumIsIn = thisDatumIsIn && self.matchesSingleCriteria(response.rows[i].key, queryTokens[j+1]);
                } else {
                  // Do a union
                  thisDatumIsIn = thisDatumIsIn || self.matchesSingleCriteria(response.rows[i].key, queryTokens[j+1]);
                }
              }

              // 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);
              }
            }
          }

          callback(matchIds);
        });
      });
    },

    /**
     * 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 : function(objectToSearchThrough, criteria) {
      var delimiterIndex = criteria.indexOf(":");
      var label = criteria.substring(0, delimiterIndex);
      var value = criteria.substring(delimiterIndex + 1);

      return objectToSearchThrough[label] && (objectToSearchThrough[label].toLowerCase().indexOf(value) >= 0);
    },

    /**
     * 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 : function(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 (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) {
          currentString = currentString + " " + currentItem.toLowerCase();
        } else {
          currentString = currentItem.toLowerCase();
        }
      }
      queryTokens.push(currentString);

      return queryTokens;
    },

    /**
     * Clone the current Datum and return the clone. The clone is put in the current
     * Session, regardless of the origin Datum's Session.
     *
     * @return The clone of the current Datum.
     */
    clone : function() {
      // Create a new Datum based on the current Datum
      var datum = new Datum({
        audioVideo : new AudioVideo(this.get("audioVideo").toJSON(), {parse: true}),
        comments : new Comments(this.get("comments").toJSON(), {parse: true}),
        dateEntered : this.get("dateEntered"),
        datumFields : new DatumFields(this.get("datumFields").toJSON(), {parse: true}),
        datumState : new DatumState(this.get("datumState").toJSON(), {parse: true}),
        datumStates : new DatumStates(this.get("datumStates").toJSON(), {parse: true}),
        datumTags : new DatumTags(this.get("datumTags").toJSON(), {parse: true})
        // Don't need to do Session here since it will be overwritten in DatumContainerEditView.prependDatum()
      });

      return datum;
    }
  });

  return Datum;
});
<|endoftext|>


<commit_before>define([
    "use!backbone",
    "audio_video/AudioVideo",
    "comment/Comments",
    "datum/DatumField",
    "datum/DatumFields",
    "datum/DatumState",
    "datum/DatumStates",
    "datum/DatumTag",
    "datum/DatumTags",
    "datum/Session",
    "libs/Utils"
], function(
    Backbone,
    AudioVideo,
    Comments,
    DatumField,
    DatumFields,
    DatumState,
    DatumStates,
    DatumTag,
    DatumTags,
    Session
) {
  var Datum = Backbone.Model.extend(
  /** @lends Datum.prototype */
  {
    /**
     * @class The Datum widget is the place where all linguistic data is
     *        entered; one at a time.
     *
     * @property {DatumField} utterance The utterance field generally
     *           corresponds to the first line in linguistic examples that can
     *           either be written in the language's orthography or a
     *           romanization of the language. An additional field can be added
     *           if the language has a non-roman script.
     * @property {DatumField} gloss The gloss field corresponds to the gloss
     *           line in linguistic examples where the morphological details of
     *           the words are displayed.
     * @property {DatumField} translation The translation field corresponds to
     *           the third line in linguistic examples where in general an
     *           English translation. An additional field can be added if
     *           translations into other languages is needed.
     * @property {DatumField} judgment The judgment is the grammaticality
     *           judgment associated with the datum, so grammatical,
     *           ungrammatical, felicitous, unfelicitous etc.
     * @property {AudioVisual} audioVisual 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.
     *
     * @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.
     *
     * @extends Backbone.Model
     * @constructs
     */
    initialize : function() {

    //if the corpusname changes, change the pouch as well so that this object goes with its corpus's local pouchdb
      this.bind("change:corpusname", function() {
        this.pouch = Backbone.sync
        .pouch(Utils.androidApp() ? Utils.touchUrl
            + this.get("corpusname") : Utils.pouchUrl
            + this.get("corpusname"));
      }, this);

      try {
        if (this.get("corpusname") == undefined) {
          this.set("corpusname", app.get("corpus").get("corpusname"));
        }
      } catch(e) {
        Utils.debug("Corpusname was undefined on this corpus, the datum will not have a valid corpusname until it is set.");
      }
      // Initialially, the first datumState is selected
      if (this.get("datumStates") && (this.get("datumStates").models.length > 0)) {
        this.get("datumStates").models[0].set("selected", "selected");
      }
    },

    defaults : {
      audioVideo : new AudioVideo(),
      comments : new Comments(),
      datumTags : new DatumTags()
    },

    model : {
      datumFields : DatumFields,
      audioVideo : AudioVideo,
      session : Session,
      comments : Comments,
      datumStates : DatumStates,
      datumTags : DatumTags
    },

    parse : function(response) {
      if (response.ok === undefined) {
        for (var key in this.model) {
          var embeddedClass = this.model[key];
          var embeddedData = response[key];
          response[key] = new embeddedClass(embeddedData, {parse:true});
        }
      }

      return response;
    },

    pouch : Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl
        : Utils.pouchUrl),

    /**
     * Gets all the DatumIds in the current Corpus sorted by their date.
     *
     * @param {Function} callback A function that expects a single parameter. That
     * parameter is the result of calling "get_datum_ids/by_date". So it is an array
     * of objects. Each object has a 'key' and a 'value' attribute. The 'key'
     * attribute contains the Datum's dateEntered and the 'value' attribute contains
     * the Datum's ID.
     */
    getAllDatumIdsByDate : function(callback) {
      this.pouch(function(err, db) {
        /*
        Code for get_datum_ids/by_date

        function(doc) {
          if (doc.dateEntered) {
            emit(doc.dateEntered, doc.id);
          }
        }
        */

        db.query("get_datum_ids/by_date", {reduce: false}, function(err, response) {
          if ((!err) && (typeof callback == "function"))  {
            console.log("Callback with: ", response.rows);
            callback(response.rows)
          }
        });
      });
    },

    searchByQueryString : function(queryString, callback) {
      var self = this;
      this.pouch(function(err, db) {
        // Code for get_datum_field/get_datum_fields
        //
        // function(doc) {
        //   if ((doc.datumFields) && (doc.session)) {
        //     var obj = {};
        //     for (i = 0; i < doc.datumFields.length; i++) {
        //       if (doc.datumFields[i].value) {
        //         obj[doc.datumFields[i].label] = doc.datumFields[i].value;
        //       }
        //     }
        //     if (doc.session.sessionFields) {
        //       for (j = 0; j < doc.session.sessionFields.length; j++) {
        //         if (doc.session.sessionFields[j].value) {
        //           obj[doc.session.sessionFields[j].label] = doc.doc.session.sessionFields[j].value;
        //         }
        //       }
        //     }
        //     emit(obj, doc._id);
        //   }
        // }
        db.query("get_datum_field/get_datum_fields", {reduce: false}, function(err, response) {
          var matchIds = [];

          if (!err) {
            // Process the given query string into tokens
            var queryTokens = self.processQueryString(queryString);

            // Go through all the rows of results
            for (i in response.rows) {
              // Determine if this datum matches the first search criteria
              var thisDatumIsIn = self.matchesSingleCriteria(response.rows[i].key, queryTokens[0]);

              // Progressively determine whether the datum still matches based on
              // subsequent search criteria
              for (j = 1; j < queryTokens.length; j += 2) {
                if (queryTokens[j] == "AND") {
                  // Short circuit: if it's already false then it continues to be false
                  if (!thisDatumIsIn) {
                    break;
                  }

                  // Do an intersection
                  thisDatumIsIn = thisDatumIsIn && self.matchesSingleCriteria(response.rows[i].key, queryTokens[j+1]);
                } else {
                  // Do a union
                  thisDatumIsIn = thisDatumIsIn || self.matchesSingleCriteria(response.rows[i].key, queryTokens[j+1]);
                }
              }

              // 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);
              }
            }
          }

          callback(matchIds);
        });
      });
    },

    /**
     * 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 : function(objectToSearchThrough, criteria) {
      var delimiterIndex = criteria.indexOf(":");
      var label = criteria.substring(0, delimiterIndex);
      var value = criteria.substring(delimiterIndex + 1);

      return objectToSearchThrough[label] && (objectToSearchThrough[label].toLowerCase().indexOf(value) >= 0);
    },

    /**
     * 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 : function(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 (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) {
          currentString = currentString + " " + currentItem.toLowerCase();
        } else {
          currentString = currentItem.toLowerCase();
        }
      }
      queryTokens.push(currentString);

      return queryTokens;
    },

    /**
     * Clone the current Datum and return the clone. The clone is put in the current
     * Session, regardless of the origin Datum's Session.
     *
     * @return The clone of the current Datum.
     */
    clone : function() {
      // Create a new Datum based on the current Datum
      var datum = new Datum({
        audioVideo : new AudioVideo(this.get("audioVideo").toJSON(), {parse: true}),
        comments : new Comments(this.get("comments").toJSON(), {parse: true}),
        dateEntered : this.get("dateEntered"),
        datumFields : new DatumFields(this.get("datumFields").toJSON(), {parse: true}),
        datumStates : new DatumStates(this.get("datumStates").toJSON(), {parse: true}),
        datumTags : new DatumTags(this.get("datumTags").toJSON(), {parse: true}),
        corpusname : this.get("corpusname")
        // Don't need to do Session here since it will be overwritten in DatumContainerEditView.prependDatum()
      });

      return datum;
    }
  });

  return Datum;
});
<commit_msg>Adding a change corpus method to datum, and trying to set the corpusname when the datum is first created<commit_after>define([
    "use!backbone",
    "audio_video/AudioVideo",
    "comment/Comments",
    "datum/DatumField",
    "datum/DatumFields",
    "datum/DatumState",
    "datum/DatumStates",
    "datum/DatumTag",
    "datum/DatumTags",
    "datum/Session",
    "libs/Utils"
], function(
    Backbone,
    AudioVideo,
    Comments,
    DatumField,
    DatumFields,
    DatumState,
    DatumStates,
    DatumTag,
    DatumTags,
    Session
) {
  var Datum = Backbone.Model.extend(
  /** @lends Datum.prototype */
  {
    /**
     * @class The Datum widget is the place where all linguistic data is
     *        entered; one at a time.
     *
     * @property {DatumField} utterance The utterance field generally
     *           corresponds to the first line in linguistic examples that can
     *           either be written in the language's orthography or a
     *           romanization of the language. An additional field can be added
     *           if the language has a non-roman script.
     * @property {DatumField} gloss The gloss field corresponds to the gloss
     *           line in linguistic examples where the morphological details of
     *           the words are displayed.
     * @property {DatumField} translation The translation field corresponds to
     *           the third line in linguistic examples where in general an
     *           English translation. An additional field can be added if
     *           translations into other languages is needed.
     * @property {DatumField} judgment The judgment is the grammaticality
     *           judgment associated with the datum, so grammatical,
     *           ungrammatical, felicitous, unfelicitous etc.
     * @property {AudioVisual} audioVisual 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.
     *
     * @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.
     *
     * @extends Backbone.Model
     * @constructs
     */
    initialize : function() {

    //if the corpusname changes, change the pouch as well so that this object goes with its corpus's local pouchdb
      this.bind("change:corpusname", function() {
        this.pouch = Backbone.sync
        .pouch(Utils.androidApp() ? Utils.touchUrl
            + this.get("corpusname") : Utils.pouchUrl
            + this.get("corpusname"));
      }, this);

      try {
        if (this.get("corpusname") == undefined) {
          this.set("corpusname", app.get("corpus").get("corpusname"));
        }
        this.pouch = Backbone.sync
        .pouch(Utils.androidApp() ? Utils.touchUrl
            + this.get("corpusname") : Utils.pouchUrl
            + this.get("corpusname"));
      } catch(e) {
        Utils.debug("Corpusname was undefined on this corpus, the datum will not have a valid corpusname until it is set.");
      }
      // Initialially, the first datumState is selected
      if (this.get("datumStates") && (this.get("datumStates").models.length > 0)) {
        this.get("datumStates").models[0].set("selected", "selected");
      }
    },

    defaults : {
      audioVideo : new AudioVideo(),
      comments : new Comments(),
      datumTags : new DatumTags()
    },

    model : {
      datumFields : DatumFields,
      audioVideo : AudioVideo,
      session : Session,
      comments : Comments,
      datumStates : DatumStates,
      datumTags : DatumTags
    },

    parse : function(response) {
      if (response.ok === undefined) {
        for (var key in this.model) {
          var embeddedClass = this.model[key];
          var embeddedData = response[key];
          response[key] = new embeddedClass(embeddedData, {parse:true});
        }
      }

      return response;
    },

    pouch : Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl
        : Utils.pouchUrl),

    changeCorpus : function(corpusname) {
      this.pouch = Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl + corpusname : Utils.pouchUrl + corpusname);
    },
    /**
     * Gets all the DatumIds in the current Corpus sorted by their date.
     *
     * @param {Function} callback A function that expects a single parameter. That
     * parameter is the result of calling "get_datum_ids/by_date". So it is an array
     * of objects. Each object has a 'key' and a 'value' attribute. The 'key'
     * attribute contains the Datum's dateEntered and the 'value' attribute contains
     * the Datum's ID.
     */
    getAllDatumIdsByDate : function(callback) {
      this.pouch(function(err, db) {
        /*
        Code for get_datum_ids/by_date

        function(doc) {
          if (doc.dateEntered) {
            emit(doc.dateEntered, doc.id);
          }
        }
        */

        db.query("get_datum_ids/by_date", {reduce: false}, function(err, response) {
          if ((!err) && (typeof callback == "function"))  {
            console.log("Callback with: ", response.rows);
            callback(response.rows)
          }
        });
      });
    },

    searchByQueryString : function(queryString, callback) {
      var self = this;
      this.pouch(function(err, db) {
        // Code for get_datum_field/get_datum_fields
        //
        // function(doc) {
        //   if ((doc.datumFields) && (doc.session)) {
        //     var obj = {};
        //     for (i = 0; i < doc.datumFields.length; i++) {
        //       if (doc.datumFields[i].value) {
        //         obj[doc.datumFields[i].label] = doc.datumFields[i].value;
        //       }
        //     }
        //     if (doc.session.sessionFields) {
        //       for (j = 0; j < doc.session.sessionFields.length; j++) {
        //         if (doc.session.sessionFields[j].value) {
        //           obj[doc.session.sessionFields[j].label] = doc.doc.session.sessionFields[j].value;
        //         }
        //       }
        //     }
        //     emit(obj, doc._id);
        //   }
        // }
        db.query("get_datum_field/get_datum_fields", {reduce: false}, function(err, response) {
          var matchIds = [];

          if (!err) {
            // Process the given query string into tokens
            var queryTokens = self.processQueryString(queryString);

            // Go through all the rows of results
            for (i in response.rows) {
              // Determine if this datum matches the first search criteria
              var thisDatumIsIn = self.matchesSingleCriteria(response.rows[i].key, queryTokens[0]);

              // Progressively determine whether the datum still matches based on
              // subsequent search criteria
              for (j = 1; j < queryTokens.length; j += 2) {
                if (queryTokens[j] == "AND") {
                  // Short circuit: if it's already false then it continues to be false
                  if (!thisDatumIsIn) {
                    break;
                  }

                  // Do an intersection
                  thisDatumIsIn = thisDatumIsIn && self.matchesSingleCriteria(response.rows[i].key, queryTokens[j+1]);
                } else {
                  // Do a union
                  thisDatumIsIn = thisDatumIsIn || self.matchesSingleCriteria(response.rows[i].key, queryTokens[j+1]);
                }
              }

              // 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);
              }
            }
          }

          callback(matchIds);
        });
      });
    },

    /**
     * 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 : function(objectToSearchThrough, criteria) {
      var delimiterIndex = criteria.indexOf(":");
      var label = criteria.substring(0, delimiterIndex);
      var value = criteria.substring(delimiterIndex + 1);

      return objectToSearchThrough[label] && (objectToSearchThrough[label].toLowerCase().indexOf(value) >= 0);
    },

    /**
     * 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 : function(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 (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) {
          currentString = currentString + " " + currentItem.toLowerCase();
        } else {
          currentString = currentItem.toLowerCase();
        }
      }
      queryTokens.push(currentString);

      return queryTokens;
    },

    /**
     * Clone the current Datum and return the clone. The clone is put in the current
     * Session, regardless of the origin Datum's Session.
     *
     * @return The clone of the current Datum.
     */
    clone : function() {
      // Create a new Datum based on the current Datum
      var datum = new Datum({
        audioVideo : new AudioVideo(this.get("audioVideo").toJSON(), {parse: true}),
        comments : new Comments(this.get("comments").toJSON(), {parse: true}),
        dateEntered : this.get("dateEntered"),
        datumFields : new DatumFields(this.get("datumFields").toJSON(), {parse: true}),
        datumStates : new DatumStates(this.get("datumStates").toJSON(), {parse: true}),
        datumTags : new DatumTags(this.get("datumTags").toJSON(), {parse: true}),
        corpusname : this.get("corpusname")
        // Don't need to do Session here since it will be overwritten in DatumContainerEditView.prependDatum()
      });

      return datum;
    }
  });

  return Datum;
});
<|endoftext|>


<commit_before>/* ctfile.js
 * Copyright 2016 Dmitry Sigaev
 *
 * Released under the MIT license -- see MIT-LICENSE for details
 */

/*
 *    Parser for Chemical Table Files formats
 *    Usage:
 *        var ctfile = require('ctfile');
 *        var CTobject = ctfile.parseCTfileFormat(string);
 *        var version = ctfile.getVersion();
 *    @module ctfile
 *    @class  ctfile
 */

var CTFile = function () {
}

/*
 *    Return module version object.
 *    @method getVersion
 *    @return {String} parser version
 */

CTFile.prototype.getVersion = function () {
	return {
		moduleVersion: '0.0.0'
	};
};

/*
 *	Parses a string to check line by a template. Converts string data to boolean
 *	@method parseFlag
 *	@param {String} flag the string to parse
 *	@return {Bolean} type from the flag string
 */
var parseFlag = function (flag) {
	return (/^\s*((true)|1)\s*$/.test(flag))? true : false;
};

/*
 *    Parses a CTAB record and returns an Object
 *    @method parseCTfileFormat
 *    @param {String} mol the complete molfile, including newlines
 *    @return {Object} an object representing the contents of the molfile
 */

CTFile.prototype.parseCTfileFormat = function (mol) {
	return {
		ctab: {}
	};
};

/*
 *	UT:Parses a string to check line by a template. Converts string data to boolean
 *	@method parseFlag
 *	@param {String} flag the string to parse
 *	@return {Bolean} type from the flag string
 */
CTFile.prototype.ut_parseFlag = function (flag) {
	return parseFlag(flag);
};

module.exports = new CTFile ();
<commit_msg>add molfileHeaderTemplate Object Header Template for V2000 and V3000 Molfiles. Identifies the molfile: molecule name, user's name, program, date, and other miscellaneous information and comments".<commit_after>/* ctfile.js
 * Copyright 2016 Dmitry Sigaev
 *
 * Released under the MIT license -- see MIT-LICENSE for details
 */

/*
 *    Parser for Chemical Table Files formats
 *    Usage:
 *        var ctfile = require('ctfile');
 *        var CTobject = ctfile.parseCTfileFormat(string);
 *        var version = ctfile.getVersion();
 *    @module ctfile
 *    @class  ctfile
 */

var CTFile = function () {
}

/*
 *    Return module version object.
 *    @method getVersion
 *    @return {String} parser version
 */

CTFile.prototype.getVersion = function () {
	return {
		moduleVersion: '0.0.0'
	};
};

/*
 *	Parses a string to check line by a template. Converts string data to boolean
 *	@method parseFlag
 *	@param {String} flag the string to parse
 *	@return {Bolean} type from the flag string
 */
var parseFlag = function (flag) {
	return (/^\s*((true)|1)\s*$/.test(flag))? true : false;
};

/*
 * Molfile
 * By now there are two version of Molfile format: V2000 and V3000
 * V2000 and V3000 have common header structure but they have different ctab data.
 * V2000 header contains the information without which it can not continue to read data
 * the mainest line where this information located is 4
 */
var molfileHeaderTemplate = {
	description: "Header Template for V2000 and V3000 Molfiles. Identifies the molfile: molecule name, user's name, program, date, and other miscellaneous information and comments",
	line1 : {
		description: "Molecule name. This line is unformatted, but like all other lines in a molfile cannot extend beyond column 80. If no name is available, a blank line must be present.",
		rep: function (x) { return x.replace(/;/g, ' '); }, // type = 1;
		check_zero: /(\d[23]D)|(V[23]000)/, /* regular expression object for matching text in line with keys of next lines*/
		mask: 'N40N40', /* (FORTRAN notation: A2<--A8--><---A10-->A2I2<--F10.5-><---F12.5--><-I6-> ) */
		format : { 'N': { key: 'name', fn: String, def: " " } }
	},
	line2 : {
		description: "Note: A blank line can be substituted for line 2.",
		mask: 'IIPPPPPPPPMMDDYYHHmmddSSssssssssssEEEEEEEEEEEERRRRRR', /*(FORTRAN: A2<--A8--><---A10-->A2I2<--F10.5-><---F12.5--><-I6-> ) */
		format: {
			'I': { key: 'userInitials', fn: String },
			'P': { key: 'programName', fn: String },
			'M': { key: 'month', fn: parseInt, def: 1 },
			'D': { key: 'day', fn: parseInt, def: 0 },
			'Y': { key: 'year', fn: parseInt, def: 0 },
			'H': { key: 'hour', fn: parseInt, def: 0 },
			'm': { key: 'minute', fn: parseInt, def: 0 },
			'd': { key: 'dimension', fn: String },
			'S': { key: 'scalingFactorsMajor', fn: String },
			's': { key: 'scalingFactorsMinor', fn: String },
			'E': { key: 'energy', fn: String },
			'R': { key: 'internalRegistryNumber', fn: String }
		}
	},
	line3 : {
		description: "A line for comments.If no comment is available, a blank line must be present.",
		rep: function (x) { return x.replace(/;/g, ' '); }, // type = 1;
		check_zero: /V[23]000/,
		mask: 'N40N40',
		format : { 'N': { key: 'comments', fn: String, def: " " } }
	},
	line4 : {
		description: "V2000 commonly",
		mask: 'aaabbblllfffcccsssxxxrrrpppiiimmmvvvvvv',
		format: {
			'a': { key: 'numAtoms', fn: parseInt, def: 0 },
			'b': { key: 'numBonds', fn: parseInt, def: 0 },
			'l': { key: 'numAtomLists', fn: parseInt, def: 0 },
			'c': { key: 'chiralFlag', fn: parseFlag, def: false },
			's': { key: 'numStextEntries', fn: parseInt, def: 0 },
			'm': { key: 'numAdditionalProperties', fn: parseInt, def: 999 },
			'v': { key: 'version', fn: String, def: 0 }
		}
	}
};

/*
 *    Parses a CTAB record and returns an Object
 *    @method parseCTfileFormat
 *    @param {String} mol the complete molfile, including newlines
 *    @return {Object} an object representing the contents of the molfile
 */

CTFile.prototype.parseCTfileFormat = function (mol) {
	return {
		ctab: {}
	};
};

/*
 *	UT:Parses a string to check line by a template. Converts string data to boolean
 *	@method parseFlag
 *	@param {String} flag the string to parse
 *	@return {Bolean} type from the flag string
 */
CTFile.prototype.ut_parseFlag = function (flag) {
	return parseFlag(flag);
};

module.exports = new CTFile ();
<|endoftext|>


<commit_before>define([
    "use!backbone",
    "datum_status/DatumStatus",
    "datum_tag/DatumTag",
    "datum_field/DatumField",
    "session/Session",
    "libs/Utils"
], function(
    Backbone,
    DatumStatus,
    DatumMenu,
    DatumTag,
    DatumField,
    Session
) {
  var Datum = Backbone.Model.extend(
  /** @lends Datum.prototype */
  {
    /**
     * @class The Datum widget is the place where all linguistic data is
     *      entered; one at a time.
     *
     * @property {String} utterance The utterance field generally
     *      corresponds to the first line in linguistic examples that can
     *      either be written in the language's orthography or a
     *      romanization of the language. An additional field can be added
     *      if the language has a non-roman script.
     * @property {String} gloss The gloss field corresponds to the gloss
     *      line in linguistic examples where the morphological details of
     *      the words are displayed.
     * @property {String} translation The translation field corresponds to
     *      the third line in linguistic examples where in general an
     *      English translation.  An additional field can be added if
     *      translations into other languages is needed.
     * @property {Number} sessionID The session ID corresponds to the
     *      number assigned to the session in which the datum is being
     *      placed.  The session will contain details such as date,
     *      language, informant etc.
     * @property {DatumStatus} status When a datum is created, it can be
     * tagged with a status, such as 'to be checked with an informant'.
     * @property {DatumField} DatumField The extra fields correspond to the
     *      user's preset of chosen fields, which may extend beyond the
     *      standard three.
     *
     * @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.
     *
     * @extends Backbone.Model
     * @constructs
     */
    initialize : function() {
    },

    defaults : {
      //here are the attributes a datum minimally has to have, other fields can be added when the user designs their own fields later.
      utterance : "tusunayawan",
      morphemes: "tusa-naya-wa-n",
      gloss : "dance-IMP-1OM-3SG",
      translation : "I feel like dancing.",

      //While it will not look like a field, it will essentially be a place where the user can click and add tags and then they will appear in little bubbles.
      grammaticalTags : "impulsative",

      sessionID : 0,
      status : new DatumStatus(),
      datumField : new DatumField(),
      datumTag : new DatumTag()
    },

    pouch: Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl : Utils.pouchUrl),

    /**
     * The LaTeXiT function automatically mark-ups an example in LaTeX code
     * (\exg. \"a) and then copies it on the clipboard so that when the user
     * switches over to their LaTeX file they only need to paste it in.
     */
    laTeXiT: function() {
 	    return "";
    },

    /**
     * The addAudio function is a drop box in which the user can drag an
     * audio file and link it to the relevant datum.
     */
    addAudio: function() {
 	    return true;
    },

    /**
     * The playDatum function appears when the audio has already been added
     * and allows the user to play the associated audio file.
     */
    playDatum: function() {
 	    return true;
    },

    /**
     * The copyDatum function copies all datum fields to the clipboard.
     */
    copyDatum: function() {
 	    return "";
    },

    /**
     * The duplicateDatum function opens a new datum field set with the fields
     * already filled exactly like the previous datum so that the user can
     * minimally edit the datum.
     */
    duplicateDatum: function() {
 	    var datum = new Datum();
 	    return datum;
    }
  });

  return Datum;
});
<commit_msg>Changed Datum defaults to empty strings.<commit_after>define([
    "use!backbone",
    "datum_status/DatumStatus",
    "datum_tag/DatumTag",
    "datum_field/DatumField",
    "session/Session",
    "libs/Utils"
], function(
    Backbone,
    DatumStatus,
    DatumMenu,
    DatumTag,
    DatumField,
    Session
) {
  var Datum = Backbone.Model.extend(
  /** @lends Datum.prototype */
  {
    /**
     * @class The Datum widget is the place where all linguistic data is
     *      entered; one at a time.
     *
     * @property {String} utterance The utterance field generally
     *      corresponds to the first line in linguistic examples that can
     *      either be written in the language's orthography or a
     *      romanization of the language. An additional field can be added
     *      if the language has a non-roman script.
     * @property {String} gloss The gloss field corresponds to the gloss
     *      line in linguistic examples where the morphological details of
     *      the words are displayed.
     * @property {String} translation The translation field corresponds to
     *      the third line in linguistic examples where in general an
     *      English translation.  An additional field can be added if
     *      translations into other languages is needed.
     * @property {Number} sessionID The session ID corresponds to the
     *      number assigned to the session in which the datum is being
     *      placed.  The session will contain details such as date,
     *      language, informant etc.
     * @property {DatumStatus} status When a datum is created, it can be
     * tagged with a status, such as 'to be checked with an informant'.
     * @property {DatumField} DatumField The extra fields correspond to the
     *      user's preset of chosen fields, which may extend beyond the
     *      standard three.
     *
     * @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.
     *
     * @extends Backbone.Model
     * @constructs
     */
    initialize : function() {
    },

    defaults : {
      //here are the attributes a datum minimally has to have, other fields can be added when the user designs their own fields later.
      utterance : "",
      morphemes: "",
      gloss : "",
      translation : "",
      grammaticalTags : "", //While it will not look like a field, it will essentially be a place where the user can click and add tags and then they will appear in little bubbles.
      sessionID : 0,
      status : new DatumStatus(),
      datumField : new DatumField(),
      datumTag : new DatumTag()
    },

    pouch: Backbone.sync.pouch(Utils.androidApp() ? Utils.touchUrl : Utils.pouchUrl),

    /**
     * The LaTeXiT function automatically mark-ups an example in LaTeX code
     * (\exg. \"a) and then copies it on the clipboard so that when the user
     * switches over to their LaTeX file they only need to paste it in.
     */
    laTeXiT: function() {
 	    return "";
    },

    /**
     * The addAudio function is a drop box in which the user can drag an
     * audio file and link it to the relevant datum.
     */
    addAudio: function() {
 	    return true;
    },

    /**
     * The playDatum function appears when the audio has already been added
     * and allows the user to play the associated audio file.
     */
    playDatum: function() {
 	    return true;
    },

    /**
     * The copyDatum function copies all datum fields to the clipboard.
     */
    copyDatum: function() {
 	    return "";
    },

    /**
     * The duplicateDatum function opens a new datum field set with the fields
     * already filled exactly like the previous datum so that the user can
     * minimally edit the datum.
     */
    duplicateDatum: function() {
 	    var datum = new Datum();
 	    return datum;
    }
  });

  return Datum;
});
<|endoftext|>


<commit_before>/* eslint-env mocha */
'use strict'

const { getDescribe, getIt, expect } = require('../utils/mocha')

module.exports = (createCommon, options) => {
  const describe = getDescribe(options)
  const it = getIt(options)
  const common = createCommon()

  describe('.id', function () {
    this.timeout(60 * 1000)
    let ipfs

    before(function (done) {
      // CI takes longer to instantiate the daemon, so we need to increase the
      // timeout for the before step

      common.setup((err, factory) => {
        expect(err).to.not.exist()
        factory.spawnNode((err, node) => {
          expect(err).to.not.exist()
          ipfs = node
          done()
        })
      })
    })

    after((done) => {
      common.teardown(done)
    })

    it('should get the node ID', (done) => {
      ipfs.id((err, res) => {
        expect(err).to.not.exist()
        expect(res).to.have.a.property('id')
        expect(res).to.have.a.property('publicKey')
        done()
      })
    })

    it('should get the node ID (promised)', () => {
      return ipfs.id()
        .then((res) => {
          expect(res).to.have.a.property('id')
          expect(res).to.have.a.property('publicKey')
        })
    })
  })
}
<commit_msg>chore: remove comment<commit_after>/* eslint-env mocha */
'use strict'

const { getDescribe, getIt, expect } = require('../utils/mocha')

module.exports = (createCommon, options) => {
  const describe = getDescribe(options)
  const it = getIt(options)
  const common = createCommon()

  describe('.id', function () {
    this.timeout(60 * 1000)
    let ipfs

    before(function (done) {
      common.setup((err, factory) => {
        expect(err).to.not.exist()
        factory.spawnNode((err, node) => {
          expect(err).to.not.exist()
          ipfs = node
          done()
        })
      })
    })

    after((done) => {
      common.teardown(done)
    })

    it('should get the node ID', (done) => {
      ipfs.id((err, res) => {
        expect(err).to.not.exist()
        expect(res).to.have.a.property('id')
        expect(res).to.have.a.property('publicKey')
        done()
      })
    })

    it('should get the node ID (promised)', () => {
      return ipfs.id()
        .then((res) => {
          expect(res).to.have.a.property('id')
          expect(res).to.have.a.property('publicKey')
        })
    })
  })
}
<|endoftext|>


<commit_before>var Offer = require('./mongo');

module.exports = {
  getOffers: function(req, res) {
    console.log('in getOffer');
    Offer.find().then(function(data) {
      if (data)  {
        res.status(200).send(data);
      } else {
        res.status(200).send('cannot find data');
      }
    });
  },
  createOffer: function(req, res) {
    console.log(req.body);
    var offer = new Offer(req.body);
    offer.save().then(function(data) {
      if (data) {
        console.log('success post');
        res.status(302).send(data);
      } else {
        res.status(302).send('error occured')
      }
    });
  }
}<commit_msg>removed console.log from create offer<commit_after>var Offer = require('./mongo');

module.exports = {
  getOffers: function(req, res) {
    console.log('in getOffer');
    Offer.find().then(function(data) {
      if (data)  {
        res.status(200).send(data);
      } else {
        res.status(200).send('cannot find data');
      }
    });
  },
  createOffer: function(req, res) {
    var offer = new Offer(req.body);
    offer.save().then(function(data) {
      if (data) {
        console.log('success post');
        res.status(302).send(data);
      } else {
        res.status(302).send('error occured')
      }
    });
  }
}<|endoftext|>


<commit_before>import { HueApi, lightState } from 'node-hue-api';
import five from 'johnny-five';
import pixel from 'node-pixel';

import { hueController } from '../controllers';
import { config } from '../environment';
import { handleAction, cylonEye } from '../utils';

export const EMIT_REGISTER_BOARD = 'EMIT_REGISTER_BOARD';
export const EMIT_REGISTER_BRIDGE = 'EMIT_REGISTER_BRIDGE';
export const EMIT_TURN_ON_LIGHT = 'EMIT_TURN_ON_LIGHT';
export const EMIT_TURN_OFF_LIGHT = 'EMIT_TURN_OFF_LIGHT';
export const EMIT_SERIAL_DATA_CHANGE = 'EMIT_SERIAL_DATA_CHANGE';

const initialState = {
  ports: config.ports,
  lightState: lightState.create(),
  hueUserId: config.users[Object.keys(config.users)[0]]
};

const devicesReducer = (state = initialState, action) => {
  const reducers = {
    [EMIT_REGISTER_BOARD]() {
      state.board = new five.Board({
        port: config.ports.jFive,
        repl: false
      });

      const STRIP_LENGTH = 60;

      state.board.on('ready', () => {
        const strip = new pixel.Strip({
          data: 6,
          length: STRIP_LENGTH,
          color_order: pixel.COLOR_ORDER.GRB,
          board: state.board,
          controller: 'FIRMATA',
        });

        strip.on('ready', () => {
          cylonEye.start(strip, STRIP_LENGTH);
        });
      });

      return state;
    },

    [EMIT_REGISTER_BRIDGE]() {
      state.hueBridge = new HueApi(action.bridge.ipaddress, state.hueUserId);

      return state;
    },

    [EMIT_TURN_ON_LIGHT]() {
      state.hueBridge.setLightState(2, state.lightState.on());

      return state;
    },

    [EMIT_TURN_OFF_LIGHT]() {
      state.hueBridge.setLightState(2, state.lightState.off());

      return state;
    },

    [EMIT_SERIAL_DATA_CHANGE]() {
      state.hueBridge.getLightStatus(2).then((lightStatus) => {
        if (lightStatus.state.on) {
          reducers.EMIT_TURN_OFF_LIGHT();
        } else {
          reducers.EMIT_TURN_ON_LIGHT();
        }
      });

      return state;
    }
  };

  return handleAction(state, action, reducers);
};

export default devicesReducer;
<commit_msg>Sillyness.<commit_after>import { HueApi, lightState } from 'node-hue-api';
import five from 'johnny-five';
import pixel from 'node-pixel';

import { hueController } from '../controllers';
import { config } from '../environment';
import { handleAction, cylonEye } from '../utils';

export const EMIT_REGISTER_BOARD = 'EMIT_REGISTER_BOARD';
export const EMIT_REGISTER_BRIDGE = 'EMIT_REGISTER_BRIDGE';
export const EMIT_TURN_ON_LIGHT = 'EMIT_TURN_ON_LIGHT';
export const EMIT_TURN_OFF_LIGHT = 'EMIT_TURN_OFF_LIGHT';
export const EMIT_SERIAL_DATA_CHANGE = 'EMIT_SERIAL_DATA_CHANGE';

const initialState = {
  ports: config.ports,
  lightState: lightState.create(),
  hueUserId: config.users[Object.keys(config.users)[0]]
};

const devicesReducer = (state = initialState, action) => {
  const reducers = {
    [EMIT_REGISTER_BOARD]() {
      state.board = new five.Board({
        port: config.ports.jFive,
        repl: false
      });

      const STRIP_LENGTH = 60;

      state.board.on('ready', () => {
        state.strip = new pixel.Strip({
          data: 6,
          length: STRIP_LENGTH,
          color_order: pixel.COLOR_ORDER.GRB,
          board: state.board,
          controller: 'FIRMATA',
        });
      });

      return state;
    },

    [EMIT_REGISTER_BRIDGE]() {
      state.hueBridge = new HueApi(action.bridge.ipaddress, state.hueUserId);

      return state;
    },

    [EMIT_TURN_ON_LIGHT]() {
      state.hueBridge.setLightState(2, state.lightState.on());

      return state;
    },

    [EMIT_TURN_OFF_LIGHT]() {
      state.hueBridge.setLightState(2, state.lightState.off());

      return state;
    },

    [EMIT_SERIAL_DATA_CHANGE]() {
      state.hueBridge.getLightStatus(2).then((lightStatus) => {
        if (lightStatus.state.on) {
          reducers.EMIT_TURN_OFF_LIGHT();
          cylonEye.stop();
        } else {
          reducers.EMIT_TURN_ON_LIGHT();
          cylonEye.start(state.strip, 60);
        }
      });

      return state;
    }
  };

  return handleAction(state, action, reducers);
};

export default devicesReducer;
<|endoftext|>


<commit_before>// @flow
import uuidV4 from 'uuid/v4';
import {
  computeGameNodeStates,
  validateRuleSet
} from './tree';
import {parseUser} from '../user';
import type {
  GameChannel,
  GameRules,
  GameSummary,
  GameTree,
  SgfEvent,
  SgfProp,
  SgfLoc,
  GameRuleSet,
  ConversationMessage
} from '../types';
import {GameNode} from '../types';

export function parseGameSummary(values: Object): GameSummary {
  let rules: GameRules = {
    size: values.size,
    komi: values.komi
  };
  if (values.handicap) {
    rules.handicap = values.handicap;
  }
  let summary: GameSummary = {
    type: values.gameType,
    timestamp: values.timestamp,
    rules: rules,
    players: values.players
  };
  if (values.score) {
    summary.score = values.score;
  }
  if (values.revision) {
    summary.revision = values.revision;
  }
  if (values.tag) {
    summary.tag = values.tag;
  }
  if (values.private) {
    summary.private = values.private;
  }
  if (values.inPlay) {
    summary.inPlay = values.inPlay;
  }
  return summary;
}

function parseGameComments(
  node: GameNode,
  commentDate?: ?Date
): Array<ConversationMessage> {
  let msgs = [];
  for (let prop of node.props) {
    if (prop.name !== 'COMMENT') {
      continue;
    }
    let text = prop.text;
    if (!text) {
      continue;
    }
    let lines = text.split(/[\r\n]+/m);
    let lineMsgs: Array<ConversationMessage> = [];
    for (let line of lines) {
      let match = line.match(/([a-zA-Z0-9]{1,10})[0-9\s\[\]dpk\?\-]*?:(.+)/);
      let username = match && match[1];
      let body = match && match[2];
      if (!username || !body) {
        break;
      }
      lineMsgs.push({
        id: uuidV4(),
        sender: username,
        body: body,
        date: commentDate
      });
    }
    if (lineMsgs.length) {
      msgs.push(...lineMsgs);
    }
  }
  return msgs;
}

function addPropsToNode(
  tree: GameTree,
  nodeId: number,
  props: Array<SgfProp>,
  commentDate?: ?Date
) {
  let node = tree.nodes[nodeId];
  let newProps = node ? [...node.props] : [];
  newProps.push(...props);
  let newNode = new GameNode(newProps, node ? node.children : [], node ? node.parent : null);
  tree.nodes = {
    ...tree.nodes,
    [nodeId]: newNode
  };
  tree.computedState = computeGameNodeStates(tree, nodeId);
  tree.messages = {
    ...tree.messages,
    [nodeId]: parseGameComments(newNode, commentDate)
  };
}

function addNodeChild(
  tree: GameTree,
  nodeId: number,
  childNodeId: number,
  pos: number
) {
  let node = tree.nodes[nodeId];
  let newChildren = node ? [...node.children] : [];
  if (newChildren.indexOf(childNodeId) === -1) {
    newChildren.splice(pos, 0, childNodeId);
  }
  tree.nodes = {
    ...tree.nodes,
    [nodeId]: new GameNode(node ? node.props : [], newChildren, node ? node.parent : null),
    [childNodeId]: new GameNode([], [], nodeId)
  };
}

function locsEqual(loc1: ?SgfLoc, loc2: ?SgfLoc) {
  if (!loc1 || !loc2) {
    return loc1 === loc2;
  }
  if (loc1 === 'PASS') {
    return loc1 === loc2;
  } else if (loc2 !== 'PASS') {
    return (
      loc1.x === loc2.x &&
      loc1.y === loc2.y
    );
  } else {
    return false;
  }
}

function propMatches(prop1: SgfProp, prop2: SgfProp) {
  return (
    prop1.name === prop2.name &&
    locsEqual(prop1.loc, prop2.loc)
  );
}

function removePropsFromNode(tree: GameTree, nodeId: number, props: Array<SgfProp>) {
  let node = tree.nodes[nodeId];
  let newProps = node.props.filter(nprop =>
    !props.find(prop => propMatches(prop, nprop))
  );
  let newNode = new GameNode(newProps, node.children, node.parent);
  tree.nodes = {
    ...tree.nodes,
    [nodeId]: newNode
  };
}

function replaceNodeProp(tree: GameTree, nodeId: number, prop: SgfProp) {
  let node = tree.nodes[nodeId];
  let newProps = node.props.filter(nprop =>
    propMatches(prop, nprop) ? prop : nprop
  );
  let newNode = new GameNode(newProps, node.children, node.parent);
  tree.nodes = {
    ...tree.nodes,
    [nodeId]: newNode
  };
}

function parseSgfEvents(prevTree: ?GameTree, events: Array<SgfEvent>) {
  let tree: ?GameTree = prevTree ? {...prevTree} : null;
  for (let event of events) {
    if (!tree) {
      tree = {
        nodes: {},
        computedState: {},
        messages: {},
        rootNode: event.nodeId,
        activeNode: event.nodeId
      };
    }
    if (event.type === 'PROP_ADDED') {
      addPropsToNode(tree, event.nodeId, [event.prop]);
    } else if (event.type === 'PROP_REMOVED') {
      removePropsFromNode(tree, event.nodeId, [event.prop]);
    } else if (event.type === 'PROP_CHANGED') {
      let prop = event.prop;
      if (prop.name === 'COMMENT') {
        // Special case for game chats
        addPropsToNode(tree, event.nodeId, [event.prop], new Date());
      } else {
        replaceNodeProp(tree, event.nodeId, event.prop);
      }
    } else if (event.type === 'CHILDREN_REORDERED') {
      let node = tree.nodes[event.nodeId];
      let newNode = new GameNode(node.props, event.children, node.parent);
      tree.nodes = {
        ...tree.nodes,
        [event.nodeId]: newNode
      };
    } else if (event.type === 'CHILD_ADDED') {
      addNodeChild(tree, event.nodeId, event.childNodeId, event.position || 0);
    } else if (event.type === 'PROP_GROUP_ADDED') {
      addPropsToNode(tree, event.nodeId, event.props);
    } else if (event.type === 'PROP_GROUP_REMOVED') {
      removePropsFromNode(tree, event.nodeId, event.props);
    } else if (event.type === 'ACTIVATED') {
      tree.activeNode = event.nodeId;
      tree.computedState = computeGameNodeStates(tree, tree.activeNode);
    } else {
      // Unsupported event type (e.g. live streaming) - ignore
    }
  }
  return tree;
}

function parseGameRulesFromTree(tree: GameTree): ?GameRules {
  let rootNode = tree.nodes[tree.rootNode];
  if (!rootNode) {
    return null;
  }
  let rulesProp: ?Object = rootNode.props.find(p => p.name === 'RULES');
  if (!rulesProp) {
    return null;
  }
  let size = typeof rulesProp.size === 'number' ? rulesProp.size : 19;
  let komi = typeof rulesProp.komi === 'number' ? rulesProp.komi : 6.5;
  let ruleset: ?GameRuleSet = typeof rulesProp.rules !== 'undefined' ?
    validateRuleSet(rulesProp.rules) : null;
  let rules: GameRules = {size, komi};
  if (ruleset) {
    rules.rules = ruleset;
  }
  if (typeof rulesProp.handicap === 'number') {
    rules.handicap = rulesProp.handicap;
  }
  if (typeof rulesProp.timeSystem === 'string') {
    rules.timeSystem = rulesProp.timeSystem;
  }
  if (typeof rulesProp.mainTime === 'number') {
    rules.mainTime = rulesProp.mainTime;
  }
  if (typeof rulesProp.byoYomiTime === 'number') {
    rules.byoYomiTime = rulesProp.byoYomiTime;
  }
  if (typeof rulesProp.byoYomiPeriods === 'number') {
    rules.byoYomiPeriods = rulesProp.byoYomiPeriods;
  }
  if (typeof rulesProp.byoYomiStones === 'number') {
    rules.byoYomiStones = rulesProp.byoYomiStones;
  }
  return rules;
}

// Props we store as-is without parsing
const GAME_CHAN_PROPS = [
  'initialProposal',
  'rules',
  'moveNum',
  'roomId',
  'observers',
  'name',
  'score',
  'private',
  'global',
  'over',
  'adjourned',
  'subscribers',
  'event',
  'uploaded',
  'audio',
  'paused',
  'named',
  'saved',
  'actions',
  'clocks',
  'whiteDoneSent',
  'blackDoneSent',
  'whiteScore',
  'blackScore',
  'doneId'
];

let gameTimes: {[gameTime: number]: true} = {};

function getUniqueGameTime() {
  let time = Date.now();
  while (gameTimes[time]) {
    time++;
  }
  gameTimes[time] = true;
  return time;
}

export function parseGameChannel(chan: ?GameChannel, values: Object): GameChannel {
  let newChan: Object = chan ?
    {...chan} :
    {time: getUniqueGameTime()};
  if (values.channelId) {
    newChan.id = values.channelId;
  }
  if (values.gameType) {
    newChan.type = values.gameType;
  }
  if (values.gameSummary) {
    newChan.summary = parseGameSummary(values.gameSummary);
  }
  if (values.players) {
    newChan.players = {};
    for (let role of Object.keys(values.players)) {
      newChan.players[role] = parseUser(null, values.players[role]);
    }
  }
  if (!values.rules && values.size && values.komi) {
    let rules: GameRules = {
      size: values.size,
      komi: values.komi
    };
    if (values.handicap) {
      rules.handicap = values.handicap;
    }
    newChan.rules = newChan.rules ? {...newChan.rules, ...rules} : rules;
  }
  if (values.users) {
    newChan.users = values.users.map(u => u.name);
  }
  if (values.sgfEvents) {
    let prevTree = values.type === 'GAME_JOIN' ? null : newChan.tree;
    newChan.tree = parseSgfEvents(prevTree, values.sgfEvents);
    if (newChan.tree) {
      delete newChan.tree.pendingMove;
    }
    delete newChan.undoRequest;
    if (newChan.tree) {
      let treeRules = parseGameRulesFromTree(newChan.tree);
      if (treeRules) {
        newChan.rules = {...treeRules, ...newChan.rules};
      }
    }
  }
  for (let key of GAME_CHAN_PROPS) {
    if (key in values) {
      newChan[key] = values[key];
    }
  }
  return newChan;
}
<commit_msg>remove excess whitespace from game chats<commit_after>// @flow
import uuidV4 from 'uuid/v4';
import {
  computeGameNodeStates,
  validateRuleSet
} from './tree';
import {parseUser} from '../user';
import type {
  GameChannel,
  GameRules,
  GameSummary,
  GameTree,
  SgfEvent,
  SgfProp,
  SgfLoc,
  GameRuleSet,
  ConversationMessage
} from '../types';
import {GameNode} from '../types';

export function parseGameSummary(values: Object): GameSummary {
  let rules: GameRules = {
    size: values.size,
    komi: values.komi
  };
  if (values.handicap) {
    rules.handicap = values.handicap;
  }
  let summary: GameSummary = {
    type: values.gameType,
    timestamp: values.timestamp,
    rules: rules,
    players: values.players
  };
  if (values.score) {
    summary.score = values.score;
  }
  if (values.revision) {
    summary.revision = values.revision;
  }
  if (values.tag) {
    summary.tag = values.tag;
  }
  if (values.private) {
    summary.private = values.private;
  }
  if (values.inPlay) {
    summary.inPlay = values.inPlay;
  }
  return summary;
}

function parseGameComments(
  node: GameNode,
  commentDate?: ?Date
): Array<ConversationMessage> {
  let msgs = [];
  for (let prop of node.props) {
    if (prop.name !== 'COMMENT') {
      continue;
    }
    let text = prop.text;
    if (!text) {
      continue;
    }
    let lines = text.split(/[\r\n]+/m);
    let lineMsgs: Array<ConversationMessage> = [];
    for (let line of lines) {
      let match = line.match(/([a-zA-Z0-9]{1,10})[0-9\s\[\]dpk\?\-]*?:(.+)/);
      let username = match && match[1];
      let body = match && match[2];
      if (!username || !body) {
        break;
      }
      lineMsgs.push({
        id: uuidV4(),
        sender: username,
        body: body.trim(),
        date: commentDate
      });
    }
    if (lineMsgs.length) {
      msgs.push(...lineMsgs);
    }
  }
  return msgs;
}

function addPropsToNode(
  tree: GameTree,
  nodeId: number,
  props: Array<SgfProp>,
  commentDate?: ?Date
) {
  let node = tree.nodes[nodeId];
  let newProps = node ? [...node.props] : [];
  newProps.push(...props);
  let newNode = new GameNode(newProps, node ? node.children : [], node ? node.parent : null);
  tree.nodes = {
    ...tree.nodes,
    [nodeId]: newNode
  };
  tree.computedState = computeGameNodeStates(tree, nodeId);
  tree.messages = {
    ...tree.messages,
    [nodeId]: parseGameComments(newNode, commentDate)
  };
}

function addNodeChild(
  tree: GameTree,
  nodeId: number,
  childNodeId: number,
  pos: number
) {
  let node = tree.nodes[nodeId];
  let newChildren = node ? [...node.children] : [];
  if (newChildren.indexOf(childNodeId) === -1) {
    newChildren.splice(pos, 0, childNodeId);
  }
  tree.nodes = {
    ...tree.nodes,
    [nodeId]: new GameNode(node ? node.props : [], newChildren, node ? node.parent : null),
    [childNodeId]: new GameNode([], [], nodeId)
  };
}

function locsEqual(loc1: ?SgfLoc, loc2: ?SgfLoc) {
  if (!loc1 || !loc2) {
    return loc1 === loc2;
  }
  if (loc1 === 'PASS') {
    return loc1 === loc2;
  } else if (loc2 !== 'PASS') {
    return (
      loc1.x === loc2.x &&
      loc1.y === loc2.y
    );
  } else {
    return false;
  }
}

function propMatches(prop1: SgfProp, prop2: SgfProp) {
  return (
    prop1.name === prop2.name &&
    locsEqual(prop1.loc, prop2.loc)
  );
}

function removePropsFromNode(tree: GameTree, nodeId: number, props: Array<SgfProp>) {
  let node = tree.nodes[nodeId];
  let newProps = node.props.filter(nprop =>
    !props.find(prop => propMatches(prop, nprop))
  );
  let newNode = new GameNode(newProps, node.children, node.parent);
  tree.nodes = {
    ...tree.nodes,
    [nodeId]: newNode
  };
}

function replaceNodeProp(tree: GameTree, nodeId: number, prop: SgfProp) {
  let node = tree.nodes[nodeId];
  let newProps = node.props.filter(nprop =>
    propMatches(prop, nprop) ? prop : nprop
  );
  let newNode = new GameNode(newProps, node.children, node.parent);
  tree.nodes = {
    ...tree.nodes,
    [nodeId]: newNode
  };
}

function parseSgfEvents(prevTree: ?GameTree, events: Array<SgfEvent>) {
  let tree: ?GameTree = prevTree ? {...prevTree} : null;
  for (let event of events) {
    if (!tree) {
      tree = {
        nodes: {},
        computedState: {},
        messages: {},
        rootNode: event.nodeId,
        activeNode: event.nodeId
      };
    }
    if (event.type === 'PROP_ADDED') {
      addPropsToNode(tree, event.nodeId, [event.prop]);
    } else if (event.type === 'PROP_REMOVED') {
      removePropsFromNode(tree, event.nodeId, [event.prop]);
    } else if (event.type === 'PROP_CHANGED') {
      let prop = event.prop;
      if (prop.name === 'COMMENT') {
        // Special case for game chats
        addPropsToNode(tree, event.nodeId, [event.prop], new Date());
      } else {
        replaceNodeProp(tree, event.nodeId, event.prop);
      }
    } else if (event.type === 'CHILDREN_REORDERED') {
      let node = tree.nodes[event.nodeId];
      let newNode = new GameNode(node.props, event.children, node.parent);
      tree.nodes = {
        ...tree.nodes,
        [event.nodeId]: newNode
      };
    } else if (event.type === 'CHILD_ADDED') {
      addNodeChild(tree, event.nodeId, event.childNodeId, event.position || 0);
    } else if (event.type === 'PROP_GROUP_ADDED') {
      addPropsToNode(tree, event.nodeId, event.props);
    } else if (event.type === 'PROP_GROUP_REMOVED') {
      removePropsFromNode(tree, event.nodeId, event.props);
    } else if (event.type === 'ACTIVATED') {
      tree.activeNode = event.nodeId;
      tree.computedState = computeGameNodeStates(tree, tree.activeNode);
    } else {
      // Unsupported event type (e.g. live streaming) - ignore
    }
  }
  return tree;
}

function parseGameRulesFromTree(tree: GameTree): ?GameRules {
  let rootNode = tree.nodes[tree.rootNode];
  if (!rootNode) {
    return null;
  }
  let rulesProp: ?Object = rootNode.props.find(p => p.name === 'RULES');
  if (!rulesProp) {
    return null;
  }
  let size = typeof rulesProp.size === 'number' ? rulesProp.size : 19;
  let komi = typeof rulesProp.komi === 'number' ? rulesProp.komi : 6.5;
  let ruleset: ?GameRuleSet = typeof rulesProp.rules !== 'undefined' ?
    validateRuleSet(rulesProp.rules) : null;
  let rules: GameRules = {size, komi};
  if (ruleset) {
    rules.rules = ruleset;
  }
  if (typeof rulesProp.handicap === 'number') {
    rules.handicap = rulesProp.handicap;
  }
  if (typeof rulesProp.timeSystem === 'string') {
    rules.timeSystem = rulesProp.timeSystem;
  }
  if (typeof rulesProp.mainTime === 'number') {
    rules.mainTime = rulesProp.mainTime;
  }
  if (typeof rulesProp.byoYomiTime === 'number') {
    rules.byoYomiTime = rulesProp.byoYomiTime;
  }
  if (typeof rulesProp.byoYomiPeriods === 'number') {
    rules.byoYomiPeriods = rulesProp.byoYomiPeriods;
  }
  if (typeof rulesProp.byoYomiStones === 'number') {
    rules.byoYomiStones = rulesProp.byoYomiStones;
  }
  return rules;
}

// Props we store as-is without parsing
const GAME_CHAN_PROPS = [
  'initialProposal',
  'rules',
  'moveNum',
  'roomId',
  'observers',
  'name',
  'score',
  'private',
  'global',
  'over',
  'adjourned',
  'subscribers',
  'event',
  'uploaded',
  'audio',
  'paused',
  'named',
  'saved',
  'actions',
  'clocks',
  'whiteDoneSent',
  'blackDoneSent',
  'whiteScore',
  'blackScore',
  'doneId'
];

let gameTimes: {[gameTime: number]: true} = {};

function getUniqueGameTime() {
  let time = Date.now();
  while (gameTimes[time]) {
    time++;
  }
  gameTimes[time] = true;
  return time;
}

export function parseGameChannel(chan: ?GameChannel, values: Object): GameChannel {
  let newChan: Object = chan ?
    {...chan} :
    {time: getUniqueGameTime()};
  if (values.channelId) {
    newChan.id = values.channelId;
  }
  if (values.gameType) {
    newChan.type = values.gameType;
  }
  if (values.gameSummary) {
    newChan.summary = parseGameSummary(values.gameSummary);
  }
  if (values.players) {
    newChan.players = {};
    for (let role of Object.keys(values.players)) {
      newChan.players[role] = parseUser(null, values.players[role]);
    }
  }
  if (!values.rules && values.size && values.komi) {
    let rules: GameRules = {
      size: values.size,
      komi: values.komi
    };
    if (values.handicap) {
      rules.handicap = values.handicap;
    }
    newChan.rules = newChan.rules ? {...newChan.rules, ...rules} : rules;
  }
  if (values.users) {
    newChan.users = values.users.map(u => u.name);
  }
  if (values.sgfEvents) {
    let prevTree = values.type === 'GAME_JOIN' ? null : newChan.tree;
    newChan.tree = parseSgfEvents(prevTree, values.sgfEvents);
    if (newChan.tree) {
      delete newChan.tree.pendingMove;
    }
    delete newChan.undoRequest;
    if (newChan.tree) {
      let treeRules = parseGameRulesFromTree(newChan.tree);
      if (treeRules) {
        newChan.rules = {...treeRules, ...newChan.rules};
      }
    }
  }
  for (let key of GAME_CHAN_PROPS) {
    if (key in values) {
      newChan[key] = values[key];
    }
  }
  return newChan;
}
<|endoftext|>