Source: src/js/models/filters/DateFilter.js

/* global define */
define(['jquery', 'underscore', 'backbone', 'models/filters/Filter'],
    function($, _, Backbone, Filter) {

  /**
  * @class DateFilter
  * @classdesc A search filter whose search term is an exact date or date range
  * @classcategory Models/Filters
  * @constructs DateFilter
  * @extends Filter
  */
	var DateFilter = Filter.extend(
    /** @lends DateFilter.prototype */{

    type: "DateFilter",

    /**
    * The Backbone Model attributes set on this DateFilter
    * @type {object}
    * @extends Filter#defaultts
    * @property {Date} min - The minimum Date to use in the query for this filter
    * @property {Date} max - The maximum Date to use in the query for this filter
    * @property {Date} rangeMin - The earliest possible Date that 'min' can be
    * @property {Date} rangeMax - The latest possible Date that 'max' can be
    * @property {Boolean} matchSubstring - Will always be stet to false, since Dates don't have substrings
    * @property {string} nodeName - The XML node name to use when serializing this model into XML
    */
    defaults: function(){
      return _.extend(Filter.prototype.defaults(), {
        min: 0,
        max: (new Date()).getUTCFullYear(),
        rangeMin: 1800,
        rangeMax: (new Date()).getUTCFullYear(),
        matchSubstring: false,
        nodeName: "dateFilter"
      });
    },

    /**
    * Parses the dateFilter XML node into JSON
    *
    * @param {Element} xml - The XML Element that contains all the DateFilter elements
    * @return {JSON} - The JSON object literal to be set on the model
    */
    parse: function(xml){

      try{
        var modelJSON = Filter.prototype.parse.call(this, xml);

        //Get the rangeMin and rangeMax nodes
        var rangeMinNode = $(xml).find("rangeMin"),
            rangeMaxNode = $(xml).find("rangeMax");

        //Parse the range min
        if( rangeMinNode.length ){
          modelJSON.rangeMin = new Date(rangeMinNode[0].textContent).getUTCFullYear();
        }
        //Parse the range max
        if( rangeMaxNode.length ){
          modelJSON.rangeMax = new Date(rangeMaxNode[0].textContent).getUTCFullYear();
        }

        //If this Filter is in a filter group, don't parse the values
        if( !this.get("isUIFilterType") ){
          //Get the min, max, and value nodes
          var minNode = $(xml).find("min"),
              maxNode = $(xml).find("max"),
              valueNode = $(xml).find("value");

          //Parse the min value
          if( minNode.length ){
            modelJSON.min = new Date(minNode[0].textContent).getUTCFullYear();
          }
          //Parse the max value
          if( maxNode.length ){
            modelJSON.max = new Date(maxNode[0].textContent).getUTCFullYear();
          }
          //Parse the value
          if( valueNode.length ){
            modelJSON.values = [new Date(valueNode[0].textContent).getUTCFullYear()];
          }
        }

        //If a range min and max was given, or if a min and max value was given,
        // then this DateFilter should be presented as a date range (rather than
       // an exact date value).
        if( rangeMinNode.length || rangeMinNode.length || minNode || maxNode ){
          //Set the range attribute on the JSON
          modelJSON.range = true;
        }
        else{
          //Set the range attribute on the JSON
          modelJSON.range = false;
        }
      }
      catch(e){
        //If an error occured while parsing the XML, return a blank JS object
        //(i.e. this model will just have the default values).
        return {};
      }

      return modelJSON;

    },

    /**
     * Builds a query string that represents this filter.
     *
     * @return {string} The query string to send to Solr
     */
    getQuery: function(){

      //Start the query string
      var queryString = "";

      //Only construct the query if the min or max is different than the default
      if( ((this.get("min") != this.defaults().min) && (this.get("min") != this.get("rangeMin"))) ||
           ((this.get("max") != this.defaults().max)) && (this.get("max") != this.get("rangeMax")) ){

        //Iterate over each filter field and add to the query string
        _.each(this.get("fields"), function(field, i, allFields){

          //Add the date range for this field to the query string
          queryString += field + ":" + this.getRangeQuery().replace(/ /g, "%20");

          //If there is another field, add an operator
          if( allFields[i+1] ){
            queryString += "%20" + this.get("fieldsOperator") + "%20";
          }

        }, this);

        //If there is more than one field, wrap the query in parenthesis
        if( this.get("fields").length > 1 ){
          queryString = "(" + queryString + ")";
        }

      }

      return queryString;

    },

    /**
    * Constructs a subquery string from the minimum and maximum dates.
    * @return {string} - THe subquery string
    */
    getRangeQuery: function(){
      //Get the minimum and maximum values
      var max = this.get("max"),
          min = this.get("min");

      //If no min or max was set, but there is a value, construct an exact value match query
      if( !min && min !== 0 && !max && max !== 0 &&
               (this.get("values")[0] || this.get("values")[0] === 0) ){
        return this.get("values")[0];
      }
      //If there is no min or max or value, set an empty query string
      else if( !min && min !== 0 && !max && max !== 0 &&
               ( !this.get("values")[0] && this.get("values")[0] !== 0) ){
         return "";
      }
      //If there is at least a min or max
      else{
        //If there's a min but no max, set the max to a wildcard (unbounded)
        if( (min || min === 0) && !max ){
          max = "*";
        }
        //If there's a max but no min, set the min to a wildcard (unbounded)
        else if ( !min && min !== 0 && max ){
          min = "*";
        }
        //If the max is higher than the min, set the max to a wildcard (unbounded)
        else if( (max || max === 0) && (min || min === 0) && (max < min) ){
          max = "*";
        }

        if(min != "*"){
          min = min + "-01-01T00:00:00Z";
        }
        if(max != "*"){
          max = max + "-12-31T23:59:59Z";
        }

        //Add the range for this field to the query string
        return "[" + min + "%20TO%20" + max + "]";
      }

    },

    /**
     * Updates the XML DOM with the new values from the model
     *
     *  @inheritdoc
    */
    updateDOM: function(options){

      var objectDOM = Filter.prototype.updateDOM.call(this, options);

      //Date Filters don't use matchSubstring nodes, and the value node will be recreated later
      $(objectDOM).children("matchSubstring, value").remove();

      //Get a clone of the original DOM
      var originalDOM;
      if( this.get("objectDOM") ){
        originalDOM = this.get("objectDOM").cloneNode(true);
      }

      if( typeof options == "undefined" ){
        var options = {};
      }

      // Get min and max dates
      var dateData = {
        min: this.get("min"),
        max: this.get("max"),
        value: this.get("values") ? this.get("values")[0] : null
      };

      var isRange = false;

      // Make subnodes <min> and <max> and append to DOM
      _.map(dateData, function(value, nodeName){
        
        // dateFilters don't have a min or max when the values should range from
        // a min to infinity, or from a max to infinity (e.g. "date is before...") 
        if(!value){
          return
        }

        if( nodeName == "min" ){
          var dateTime = "-01-01T00:00:00Z";
        }
        else{
          var dateTime = "-12-31T23:59:59Z";
        }

        //If this value is the same as the default value, but it wasn't previously serialized,
        if( (value == this.defaults()[nodeName]) &&
            ( !$(originalDOM).children(nodeName).length ||
              ($(originalDOM).children(nodeName).text() != value + dateTime) )){
            return;
        }

        //Create an XML node
        var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);

        //Add the date string to the XML node
        $(nodeSerialized).text( value + dateTime );

        $(objectDOM).append(nodeSerialized);

        //If either a min or max was serialized, then mark this as a range
        isRange = true;

      }, this);

      //If a value is set on this model,
      if( !isRange && this.get("values").length ){

        //Create a value XML node
        var valueNode = $(objectDOM.ownerDocument.createElement("value"));
        //Get a Date object for this value
        var date = new Date();
        date.setUTCFullYear(this.get("values")[0] + "-12-31T23:59:59Z");
        //Set the text of the XML node to the date string
        valueNode.text( date.toISOString() );
        $(objectDOM).append( valueNode );

      }


      if( this.get("isUIFilterType") ){

        // Get new date data
        var dateData = {
          rangeMin: this.get("rangeMin"),
          rangeMax: this.get("rangeMax")
        };

        // Make subnodes <min> and <max> and append to DOM
        _.map(dateData, function(value, nodeName){

          if( nodeName == "rangeMin" ){
            var dateTime = "-01-01T00:00:00Z";
          }
          else{
            var dateTime = "-12-31T23:59:59Z";
          }

          //If this value is the same as the default value, but it wasn't previously serialized,
          if( (value == this.defaults()[nodeName]) &&
              ( !$(originalDOM).children(nodeName).length ||
                ($(originalDOM).children(nodeName).text() != value + dateTime) )){
              return;
          }

          //Create an XML node
          var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);

          //Add the date string to the XML node
          $(nodeSerialized).text( value + dateTime );

          //Remove existing nodes and add the new one
          $(objectDOM).children(nodeName).remove();
          $(objectDOM).append(nodeSerialized);

        }, this);

        //Move the filterOptions node to the end of the filter node
        var filterOptionsNode = $(objectDOM).find("filterOptions");
        filterOptionsNode.detach();
        $(objectDOM).append(filterOptionsNode);
      }
      //Remove filterOptions for Date filters in collection definitions
      else{
        $(objectDOM).find("filterOptions").remove();
      }

      return objectDOM;
    },

    /**
    * Creates a human-readable string that represents the value set on this model
    * @return {string}
    */
    getReadableValue: function(){

      var readableValue = "";

      var min = this.get("min"),
          max = this.get("max"),
          value = this.get("values")[0];

      if( !value && value !== 0 ){
        //If there is a min and max
        if( (min || min === 0) && (max || max === 0) ){
          readableValue = min + " to " + max;
        }
        //If there is only a max
        else if(max || max === 0){
          readableValue = "Before " + max;
        }
        else{
          readableValue = "After " + min;
        }
      }
      else{
        readableValue = value;
      }

      return readableValue;

    },

    /**
    * @inheritdoc
    */
    hasChangedValues: function(){

      return ((this.get("min") > this.get("rangeMin") && this.get("min") !== this.defaults().min) ||
              (this.get("max") < this.get("rangeMax") && this.get("max") !== this.defaults().max))

    },

    /**
    * Checks if the values set on this model are valid and expected
    * @return {object} - Returns a literal object with the invalid attributes and their corresponding error message
    */
    validate: function(){

      //Validate most of the DateFilter attributes using the parent validate function
      var errors = Filter.prototype.validate.call(this);

      //If everything is valid so far, then we have to create a new object to store errors
      if (typeof errors != "object") {
        errors = {};
      }

      //Delete error messages for the attributes that are going to be validated specially for the DateFilter
      delete errors.values;
      delete errors.min;
      delete errors.max;

      // Check that there is a rangeMin and a rangeMax. If there isn't, then just set to
      // the default rather than creating an error.
      if (!this.get("rangeMin") && this.get("rangeMin") !== 0) {
        this.set("rangeMin", this.defaults().rangeMin)
      }
      if (!this.get("rangeMax") && this.get("rangeMax") !== 0) {
        this.set("rangeMax", this.defaults().rangeMax)
      }

      //Check that there aren't any negative numbers
      if( this.get("min") < 0 ){
        errors.min = "The minimum year cannot be a negative number."
      }
      if( this.get("max") < 0 ){
        errors.max = "The maximum year cannot be a negative number."
      }
      if( this.get("rangeMin") < 0 ){
        errors.rangeMin = "The range minimum year cannot be a negative number."
      }
      if( this.get("rangeMax") < 0 ){
        errors.rangeMax = "The range maximum year cannot be a negative number."
      }

      //Check that the min and max values are in order, if the minimum is not the default value of 0
      if( this.get("min") > this.get("max") && this.get("min") != 0 ){
        errors.min = "The minimum year is after the maximum year. The minimum year must be a year before the maximum year of " + this.get("max");
      }

      //Check that all the values are numbers
      if( !errors.min && typeof this.get("min") != "number" ){
        errors.min = "The minimum year must be a number.";
      }
      if( !errors.max && typeof this.get("max") != "number" ){
        errors.max = "The maximum year must be a number.";
      }
      if( !errors.rangeMax && typeof this.get("rangeMax") != "number" ){
        errors.rangeMax = "The maximum year in the date slider must be a number.";
      }
      
      if( !errors.rangeMin && typeof this.get("rangeMin") != "number" ){
        errors.rangeMin = "The minimum year in the date slider must be a number.";
      }

      if (Object.keys(errors).length)
        return errors;
      else {
        return;
      }

    }

  });

  return DateFilter;
});