/* global define */
define(['jquery', 'underscore', 'backbone', 'models/filters/Filter'],
function($, _, Backbone, Filter) {
/**
* @class NumericFilter
* @classdesc A search filter whose search term is always an exact number or numbber range
* @classcategory Models/Filters
* @extends Filter
* @constructs
*/
var NumericFilter = Filter.extend(
/** @lends NumericFilter.prototype */{
type: "NumericFilter",
/**
* Default attributes for this model
* @extends Filter#defaults
* @type {Object}
* @property {Date} min - The minimum number to use in the query for this filter
* @property {Date} max - The maximum number to use in the query for this filter
* @property {Date} rangeMin - The lowest possible number that 'min' can be
* @property {Date} rangeMax - The highest possible number that 'max' can be
* @property {string} nodeName - The XML node name to use when serializing this model into XML
* @property {boolean} range - If true, this Filter will use a numeric range as the search tterm instead of an exact number
* @property {number} step - The number to increase the search value by when incrementally increasing or decreasing the numeric range
*/
defaults: function(){
return _.extend(Filter.prototype.defaults(), {
nodeName: "numericFilter",
min: null,
max: null,
rangeMin: null,
rangeMax: null,
range: true,
step: 0
});
},
/**
* Parses the numericFilter XML node into JSON
*
* @param {Element} xml - The XML Element that contains all the NumericFilter 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 = parseFloat(rangeMinNode[0].textContent);
}
//Parse the range max
if( rangeMaxNode.length ){
modelJSON.rangeMax = parseFloat(rangeMaxNode[0].textContent);
}
//If this Filter is in a filter group, don't parse the values
if( !this.get("inFilterGroup") ){
//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 = parseFloat(minNode[0].textContent);
}
//Parse the max value
if( maxNode.length ){
modelJSON.max = parseFloat(maxNode[0].textContent);
}
//Parse the value
if( valueNode.length ){
modelJSON.values = [parseFloat(valueNode[0].textContent)];
}
}
//If a range min and max was given, or if a min and max value was given,
// then this NumericFilter should be presented as a numeric range (rather than
// an exact numeric value).
if( rangeMinNode.length || rangeMaxNode.length || (minNode.length && maxNode.length) ){
//Set the range attribute on the JSON
modelJSON.range = true;
}
else{
//Set the range attribute on the JSON
modelJSON.range = false;
}
//If a range step was given, save it
if( modelJSON.range ){
var stepNode = $(xml).find("step");
if( stepNode.length ){
//Parse the text content of the node into a float
modelJSON.step = parseFloat(stepNode[0].textContent);
}
}
}
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.get("rangeMin") || 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){
//Construct a query string for ranges, min, or max
if(
this.get("range") ||
(this.get("max") || this.get("max") === 0) ||
(this.get("min") || this.get("min") === 0)
){
//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) ){
queryString += field + ":" + 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) ){
queryString = "";
}
//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 = "*";
}
//Add the range for this field to the query string
queryString += field + ":[" + min + "%20TO%20" + max + "]";
}
}
//If there is a value set, construct an exact numeric match query
else if( this.get("values")[0] || this.get("values")[0] === 0 ){
console.debug( "~~~If there is a value set, construct an exact numeric match query~~~" );
queryString += field + ":" + this.get("values")[0];
}
//If there is another field, add an operator
if( allFields[i+1] && queryString.length ){
queryString += "%20" + this.get("operator") + "%20";
}
}, this);
//If there is more than one field, wrap the query in paranthesis
if( this.get("fields").length > 1 && queryString.length ){
queryString = "(" + queryString + ")";
}
}
return queryString;
},
/**
* Updates the XML DOM with the new values from the model
* @inheritdoc
* @return {XMLElement} An updated numericFilter XML element from a portal document
*/
updateDOM:function(options){
try{
if( typeof options == "undefined" ){
var options = {};
}
var objectDOM = Filter.prototype.updateDOM.call(this);
//Numeric Filters don't use matchSubstring nodes
$(objectDOM).children("matchSubstring").remove();
//Get a clone of the original DOM
var originalDOM;
if( this.get("objectDOM") ){
originalDOM = this.get("objectDOM").cloneNode(true);
}
// Get new numeric data
var numericData = {
min: this.get("min"),
max: this.get("max")
};
if( !options.forCollection ){
numericData = _.extend(numericData, {
rangeMin: this.get("rangeMin"),
rangeMax: this.get("rangeMax"),
step: this.get("step")
});
}
// Make subnodes and append to DOM
_.map(numericData, function(value, nodeName){
if( value || value === 0 ){
//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 + "-01-01T00:00:00Z") )){
return;
}
var nodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
$(nodeSerialized).text(value);
$(objectDOM).append(nodeSerialized);
}
}, this);
//Remove filterOptions for collection definition filters
if( options.forCollection ){
$(objectDOM).children("filterOptions").remove();
}
else{
//Make sure the filterOptions are listed last
//Get the filterOptions element
var filterOptions = $(objectDOM).children("filterOptions");
//If the filterOptions exist
if( filterOptions.length ){
//Detach from their current position and append to the end
filterOptions.detach();
$(objectDOM).append(filterOptions);
}
}
return objectDOM;
}
catch(e){
return "";
}
},
/**
* 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 = "No more than " + max;
}
else{
readableValue = "At least " + min;
}
}
else{
readableValue = value;
}
return readableValue;
},
/**
* @inheritdoc
*/
hasChangedValues: function(){
return (this.get("values").length > 0 ||
this.get("min") != this.defaults().min ||
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 NumericFilter 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 NumericFilter
delete errors.values;
delete errors.min;
delete errors.max;
delete errors.rangeMin;
delete errors.rangeMax;
//If there is an exact number set as the search term
if( Array.isArray(this.get("values")) && this.get("values").length ){
//Check that all the values are numbers
if(_.find(this.get("values"), function(n){ return typeof n != "number" })){
errors.values = "All of the search terms for this filter need to be numbers.";
}
}
//If there is a search term set on the model that is not an array, or number,
// or undefined, or null, then it is some other invalid value like a string or date.
else if( !Array.isArray(this.get("values")) && typeof values != "number" && typeof values != "undefined" && values !== null){
errors.values = "The search term for this filter needs to a number.";
}
//Check that the min and max values are in order, if the minimum is not the default value of 0
else if( typeof this.get("min") == "number" && typeof this.get("max") == "number" ){
if( this.get("min") > this.get("max") && this.get("min") != 0 ){
errors.min = "The minimum is after the maximum. The minimum must be a number less than the maximum, which is " + this.get("max");
}
}
//If there is only a minimum number specified, check that it is a number
else if( this.get("min") && typeof this.get("min") != "number"){
errors.min = "The minimum needs to be a number."
if( this.get("max") && typeof this.get("max") != "number" ){
errors.max = "The maximum needs to be a number."
}
}
//Check if the maximum is a value other than a number
else if( this.get("max") && typeof this.get("max") != "number"){
errors.max = "The maximum needs to be a number."
}
//If there is no min, max, or value, then return an errors
else if( !this.get("max") && this.get("max") !== 0 && !this.get("min") && this.get("min") !== 0 &&
( (!this.get("values") && this.get("values") !== 0) || (Array.isArray(this.get("values")) && !this.get("values").length) )){
errors.values = "This search filter needs an exact number or a number range to use in the search query."
}
//Return the error messages
if( Object.keys(errors).length ){
return errors;
}
else{
return;
}
}
});
return NumericFilter;
});