define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
/**
* @class Filter
* @classdesc A single search filter that is used in queries sent to the DataONE search service.
* @classcategory Models/Filters
* @extends Backbone.Model
* @constructs
*/
var FilterModel = Backbone.Model.extend(
/** @lends Filter.prototype */
{
/**
* The name of this Model
* @name Filter#type
* @type {string}
* @readonly
*/
type: "Filter",
/**
* Default attributes for this model
* @type {object}
* @returns {object}
* @property {Element} objectDOM - The XML DOM for this filter
* @property {string} nodeName - The XML node name for this filter's XML DOM
* @property {string[]} fields - The search index fields to search
* @property {string[]} values - The values to search for in the given search fields
* @property {object} valueLabels - Optional human-readable labels for the elements of
* values. Keys are the value and the human-readable label is the value at that key.
* @property {string} operator - The operator to use between values set on this model.
* "AND" or "OR"
* @property {string} fieldsOperator - The operator to use between fields set on this
* model. "AND" or "OR"
* @property {string} queryGroup - Deprecated: Add this filter along with other the
* other associated query group filters to a FilterGroup model instead. Old definition:
* The name of the group this Filter is a part of, which is primarily used when
* creating a query string from multiple Filter models. Filters in the same group will
* be wrapped in parenthesis in the query.
* @property {boolean} exclude - If true, search index docs matching this filter will
* be excluded from the search results
* @property {boolean} matchSubstring - If true, the search values will be wrapped in
* wildcard characters to match substrings
* @property {string} label - A human-readable short label for this Filter
* @property {string} placeholder - A short example or description of this Filter
* @property {string} icon - A term that identifies a single icon in a supported icon
* library
* @property {string} description - A longer description of this Filter's function
* @property {boolean} isInvisible - If true, this filter will be added to the query
* but will act in the "background", like a default filter
* @property {boolean} inFilterGroup - Deprecated: use isUIFilterType instead.
* @property {boolean} isUIFilterType - If true, this filter is one of the
* UIFilterTypes, belongs to a UIFilterGroupType model, and is used to create a custom
* Portal search filters. This changes how the XML is parsed and how the model is
* validated and serialized.
*/
defaults: function () {
return {
objectDOM: null,
nodeName: "filter",
fields: [],
values: [],
valueLabels: {},
operator: "AND",
fieldsOperator: "AND",
exclude: false,
matchSubstring: false,
label: null,
placeholder: null,
icon: null,
description: null,
isInvisible: false,
isUIFilterType: false,
};
},
/**
* Creates a new Filter model
*/
initialize: function (attributes) {
if (this.get("objectDOM")) {
this.set(this.parse(this.get("objectDOM")));
}
if (attributes && attributes.isUIFilterType) {
this.set("isUIFilterType", true);
}
//If this is an isPartOf filter, then add a label and description. Make it invisible
//depending on how MetacatUI is configured.
if (
this.get("fields").length == 1 &&
this.get("fields").includes("isPartOf")
) {
this.set({
label: "Datasets added manually",
description:
"Datasets added to this collection manually by dataset owners",
isInvisible:
MetacatUI.appModel.get("hideIsPartOfFilter") === true
? true
: false,
});
}
// Operator must be AND or OR
["fieldsOperator", "operator"].forEach(function (op) {
if (!["AND", "OR"].includes(this.get(op))) {
// Set the value to the default
this.set(op, this.defaults()[op]);
}
}, this);
},
/**
* Parses the given XML node into a JSON object to be set on the model
*
* @param {Element} xml - The XML element that contains all the filter elements
* @return {JSON} - The JSON object of all the filter attributes
*/
parse: function (xml) {
//If an XML element wasn't sent as a parameter, get it from the model
if (!xml) {
var xml = this.get("objectDOM");
//Return an empty JSON object if there is no objectDOM saved in the model
if (!xml) return {};
}
var modelJSON = {};
if ($(xml).children("field").length) {
//Parse the field(s)
modelJSON.fields = this.parseTextNode(xml, "field", true);
}
if ($(xml).children("label").length) {
//Parse the label
modelJSON.label = this.parseTextNode(xml, "label");
}
// Check if this filter contains one of the Id fields - we use OR by default for the
// operator for these fields.
var idFields = MetacatUI.appModel.get("queryIdentifierFields");
var isIdFilter = false;
if (modelJSON.fields) {
isIdFilter = _.some(idFields, function (idField) {
return modelJSON.fields.includes(idField);
});
}
//Parse the operators, if they exist
if ($(xml).find("operator").length) {
modelJSON.operator = this.parseTextNode(xml, "operator");
} else {
if (isIdFilter) {
modelJSON.operator = "OR";
}
}
if ($(xml).find("fieldsOperator").length) {
modelJSON.fieldsOperator = this.parseTextNode(xml, "fieldsOperator");
} else {
if (isIdFilter) {
modelJSON.fieldsOperator = "OR";
}
}
//Parse the exclude, if it exists
if ($(xml).find("exclude").length) {
modelJSON.exclude =
this.parseTextNode(xml, "exclude") === "true" ? true : false;
}
//Parse the matchSubstring
if ($(xml).find("matchSubstring").length) {
modelJSON.matchSubstring =
this.parseTextNode(xml, "matchSubstring") === "true" ? true : false;
}
var filterOptionsNode = $(xml).children("filterOptions");
if (filterOptionsNode.length) {
//Parse the filterOptions XML node
modelJSON = _.extend(
this.parseFilterOptions(filterOptionsNode),
modelJSON,
);
}
//If this Filter is in a filter group, don't parse the values
if (!this.get("isUIFilterType")) {
if ($(xml).children("value").length) {
//Parse the value(s)
modelJSON.values = this.parseTextNode(xml, "value", true);
}
}
return modelJSON;
},
/**
* Gets the text content of the XML node matching the given node name
*
* @param {Element} parentNode - The parent node to select from
* @param {string} nodeName - The name of the XML node to parse
* @param {boolean} isMultiple - If true, parses the nodes into an array
* @return {(string|Array)} - Returns a string or array of strings of the text content
*/
parseTextNode: function (parentNode, nodeName, isMultiple) {
var node = $(parentNode).children(nodeName);
//If no matching nodes were found, return falsey values
if (!node || !node.length) {
//Return an empty array if the isMultiple flag is true
if (isMultiple) return [];
//Return null if the isMultiple flag is false
else return null;
}
//If exactly one node is found and we are only expecting one, return the text content
else if (node.length == 1 && !isMultiple) {
if (!node[0].textContent) return null;
else return node[0].textContent;
}
//If more than one node is found, parse into an array
else {
var allContents = [];
_.each(node, function (node) {
if (node.textContent || node.textContent === 0)
allContents.push(node.textContent);
});
return allContents;
}
},
/**
* Parses the filterOptions XML node into a literal object
* @param {Element} filterOptionsNode - The filterOptions XML element to parse
* @return {Object} - The parsed filter options, in literal object form
*/
parseFilterOptions: function (filterOptionsNode) {
if (typeof filterOptionsNode == "undefined") {
return {};
}
var modelJSON = {};
try {
//The list of options to parse
var options = ["placeholder", "icon", "description"];
//Parse the text nodes for each filter option
_.each(
options,
function (option) {
if ($(filterOptionsNode).children(option).length) {
modelJSON[option] = this.parseTextNode(
filterOptionsNode,
option,
false,
);
}
},
this,
);
//Parse the generic option name and value pairs and set on the model JSON
$(filterOptionsNode)
.children("option")
.each(function (i, optionNode) {
var optName = $(optionNode).children("optionName").text();
var optValue = $(optionNode).children("optionValue").text();
modelJSON[optName] = optValue;
});
//Return the JSON to be set on this model
return modelJSON;
} catch (e) {
return {};
}
},
/**
* Builds a query string that represents this filter.
*
* @return {string} The query string to send to Solr
* @param {string} [groupLevelOperator] - "AND" or "OR". The operator used in the
* parent Filters collection to combine the filter query fragments together. If the
* group level operator is "OR" and this filter has exclude set to TRUE, then a
* positive clause is added.
*/
getQuery: function (groupLevelOperator) {
//Get the values of this filter in Array format
var values = this.get("values");
if (!Array.isArray(values)) {
values = [values];
}
//Check that there are actually values to serialize
if (!values.length) {
return "";
}
//Filter out any invalid values (can't use _.compact() because we want to keep 'false' values)
values = _.reject(values, function (value) {
return (
value === null ||
typeof value == "undefined" ||
value === NaN ||
value === "" ||
(Array.isArray(value) && !value.length)
);
});
if (!values.length) {
return "";
}
//Start a query string for this model and get the fields
var queryString = "",
fields = this.get("fields");
//If the fields are not an array, convert it to an array
if (!Array.isArray(fields)) {
fields = [fields];
}
//Iterate over each field
_.each(
fields,
function (field, i) {
//Add the query string for this field to the overall model query string
queryString += field + ":" + this.getValueQuerySubstring(values);
//Add the OR operator between field names
if (fields.length > i + 1 && queryString.length) {
queryString += "%20" + this.get("fieldsOperator") + "%20";
}
},
this,
);
//If there is more than one field, wrap the multiple fields in parenthesis
if (fields.length > 1) {
queryString = "(" + queryString + ")";
}
//If this filter should be excluding matches from the results,
// then add a hyphen in front
if (queryString && this.get("exclude")) {
queryString = "-" + queryString;
if (this.requiresPositiveClause(groupLevelOperator)) {
queryString = queryString + "%20AND%20*:*";
if (groupLevelOperator && groupLevelOperator === "OR") {
queryString = "(" + queryString + ")";
}
}
}
return queryString;
},
/**
* For "negative" Filters (filter models where exclude is set to true), detects
* whether the query requires an additional "positive" query phrase in order to avoid
* the problem of pure negative queries returning zero results. If this Filter is not
* part of a collection of Filters, assume it needs the positive clause. If this
* Filter is part of a collection of Filters, detect whether there are other,
* "positive" filters in the same query (i.e. filter models where exclude is set to
* false). If there are other positive queries, then an additional clause is not
* required. If the Filter is part of a pure negative query, but it is not the last
* filter, then don't add a clause since it will be added to the last, and only one
* is required. When looking for other positive and negative filters, exclude empty
* filters and filters that use any of the identifier fields, as these are appended to
* the end of the query.
* @see {@link https://github.com/NCEAS/metacatui/issues/1600}
* @see {@link https://cwiki.apache.org/confluence/display/SOLR/NegativeQueryProblems}
* @param {string} [groupLevelOperator] - "AND" or "OR". The operator used in the
* parent Filters collection to combine the filter query fragments together. If the
* group level operator is "OR" and this filter has exclude set to TRUE, then a
* positive clause is required.
* @return {boolean} returns true of this Filter needs a positive clause, false
* otherwise
*/
requiresPositiveClause: function (groupLevelOperator) {
try {
// Only negative queries require the additional clause
if (this.get("exclude") == false) {
return false;
}
// If this Filter is not part of a collection of Filters, assume it needs the
// positive clause.
if (!this.collection) {
return true;
}
// If this Filter is the only one in the group, assume it needs a positive clause
if (this.collection.length === 1) {
return true;
}
// If this filter is being "OR"'ed together with other filters, then assume it
// needs the additional clause.
if (groupLevelOperator && groupLevelOperator === "OR") {
return true;
}
// Get all of the other filters in the same collection that are not ID filters.
// These filters are always appended to the end of the query as a separated group.
var nonIDFilters = this.collection.getNonIdFilters();
// Exclude filters that would give an empty query string (e.g. because value is
// missing)
var filters = _.reject(nonIDFilters, function (filterModel) {
if (filterModel === this) {
return false;
}
return !filterModel.isValid();
});
// If at least one filter in the collection is positive (exclude = false), then we
// don't need to add anything
var positiveFilters = _.find(filters, function (filterModel) {
return filterModel.get("exclude") != true;
});
if (positiveFilters) {
return false;
}
// Assuming that all the non-ID filters are negative, check if this is the first
// last the list. Since we only need one additional positive query phrase to avoid
// the pure negative query problem, by convention, only add the positive phrase at
// the end of the filter group
if (this === _.last(filters)) {
return true;
} else {
return false;
}
} catch (error) {
console.log(
"There was a problem detecting whether a Filter required a positive" +
" clause. Assuming that it needs one. Error details: " +
error,
);
return true;
}
},
/**
* Constructs a query substring for each of the values set on this model
*
* @example
* values: ["walker", "jones"]
* Returns: "(walker%20OR%20jones)"
*
* @param {string[]} [values] - The values to use in this query substring.
* If not provided, the values set on the model are used.
* @return {string} The query substring
*/
getValueQuerySubstring: function (values) {
//Start a query string for this field and get the values
var valuesQueryString = "",
values = values || this.get("values");
//If the values are not an array, convert it to an array
if (!Array.isArray(values)) {
values = [values];
}
//Iterate over each value set on the model
_.each(
values,
function (value, i) {
//If the value is not a string, then convert it to a string
if (typeof value != "string") {
value = value.toString();
}
//Trim off whitespace
value = value.trim();
var dateRangeRegEx =
/^\[((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d*Z)|\*)( |%20)TO( |%20)((\d{4}-[01]\d-[0-3]\dT[0-2]\d(:|\\:)[0-5]\d(:|\\:)[0-5]\d\.\d*Z)|\*)\]/,
isDateRange = dateRangeRegEx.test(value),
isSearchPhrase = value.indexOf(" ") > -1,
isIdFilter = this.isIdFilter(),
//Test for ORCIDs and group subjects
isSubject =
/^(?:https?:\/\/orcid\.org\/)?(?:\w{4}-){3}\w{4}$|^(?:CN=.{1,},DC=.{1,},DC=.{1,})$/i.test(
value,
);
// Escape special characters
value = this.escapeSpecialChar(value);
//URL encode the search value
value = encodeURIComponent(value);
// If the value is a search phrase (more than one word), is part of an ID filter,
// and not a date range string, wrap in quotes
if ((isSearchPhrase || isIdFilter || isSubject) && !isDateRange) {
value = '"' + value + '"';
}
if (this.get("matchSubstring") && !isDateRange) {
// Look for existing wildcard characters at the end of the value string, wrap
// the value string in wildcard characters if there aren't any yet.
if (!value.match(/^\*|\*$/)) {
value = "*" + value + "*";
}
}
// Add the value to the query string
valuesQueryString += value;
//Add the operator between values
if (values.length > i + 1 && valuesQueryString.length) {
valuesQueryString += "%20" + this.get("operator") + "%20";
}
},
this,
);
if (values.length > 1) {
valuesQueryString = "(" + valuesQueryString + ")";
}
return valuesQueryString;
},
/**
* Checks if any of the fields in this Filter match one of the
* {@link AppConfig#queryIdentifierFields}
* @since 2.17.0
*/
isIdFilter: function () {
try {
let fields = this.get("fields");
let values = this.get("values");
if (!fields) {
return false;
}
let idFields = MetacatUI.appModel.get("queryIdentifierFields");
let match = _.some(idFields, (idField) => fields.includes(idField));
//Check if the values are all identifiers by checking for uuids and dois
if (!match && values.length) {
match = values.every((v) =>
/^urn:uuid:\w{8,}-\w{4,}-\w{4,}-\w{4,}-\w{12,}$|^.{0,}doi.{1,}10.\d{4,9}\/[-._;()\/:A-Z0-9]+$/i.test(
v,
),
);
}
return match;
} catch (error) {
console.log(
"Error checking if a Filter model is an ID filter. " +
"Assuming it is not. Error details:" +
error,
);
return false;
}
},
/**
* Resets the values attribute on this filter
*/
resetValue: function () {
this.set("values", this.defaults().values);
},
/**
* Checks if this Filter has values different than the default values.
* @return {boolean} - Returns true if this Filter has values set on it, otherwise will return false
*/
hasChangedValues: function () {
return this.get("values").length > 0;
},
/**
* isEmpty - Checks whether this Filter has any values or fields set
*
* @return {boolean} returns true if the Filter's values and fields are empty
*/
isEmpty: function () {
try {
var fields = this.get("fields"),
values = this.get("values"),
noFields = !fields || fields.length == 0,
fieldsEmpty = _.every(fields, function (item) {
return item == "";
}),
noValues = !values || values.length == 0,
valuesEmpty = _.every(values, function (item) {
return item == "";
});
var noMinNoMax = _.every(
[this.get("min"), this.get("max")],
function (num) {
return typeof num === "undefined" || (!num && num !== 0);
},
);
// Values aren't required for UI filter types. Labels, icons, and descriptions are
// available.
if (this.get("isUIFilterType")) {
noUIVals = _.every(
["label", "icon", "description"],
function (attrName) {
var setValue = this.get(attrName);
var defaultValue = this.defaults()[attrName];
return !setValue || setValue === defaultValue;
},
this,
);
return noUIVals && noFields && fieldsEmpty && noMinNoMax;
}
// For regular search filters, just a field and some sort of search term/value is
// required
return (
noFields && fieldsEmpty && noValues && valuesEmpty && noMinNoMax
);
} catch (e) {
console.log(
"Failed to check if a Filter is empty, error message: " + e,
);
}
},
/**
* Escapes Solr query reserved characters so that search terms can include
* those characters without throwing an error.
*
* @param {string} term - The search term or phrase to escape
* @return {string} - The search term or phrase, after special characters are escaped
*/
escapeSpecialChar: function (term) {
if (!term || typeof term != "string") {
return "";
}
// Removes all the ampersands since Metacat cannot handle escaped ampersands for some reason
// See https://github.com/NCEAS/metacat/issues/1576
term = term.replaceAll("&", "");
return term.replace(
/\+|-|&|\||!|\(|\)|\{|\}|\[|\]|\^|\\|\"|~|\?|:|\//g,
"\\$&",
);
},
/**
* Updates XML DOM with the new values from the model
*
* @param {object} [options] A literal object with options for this serialization
* @return {Element} A new XML element with the updated values
*/
updateDOM: function (options) {
try {
if (typeof options == "undefined") {
var options = {};
}
var objectDOM = this.get("objectDOM"),
filterOptionsNode;
if (
typeof objectDOM == "undefined" ||
!objectDOM ||
!$(objectDOM).length
) {
// Node name differs for different filters, all of which use this function
var nodeName = this.get("nodeName") || "filter";
// Create an XML filter element from scratch
var objectDOM = new DOMParser().parseFromString(
"<" + nodeName + "></" + nodeName + ">",
"text/xml",
);
var $objectDOM = $(objectDOM).find(nodeName);
} else {
objectDOM = objectDOM.cloneNode(true);
var $objectDOM = $(objectDOM);
//Detach the filterOptions so they are saved
filterOptionsNode = $objectDOM.children("filterOptions");
filterOptionsNode.detach();
//Empty the DOM
$objectDOM.empty();
}
var xmlDocument = $objectDOM[0].ownerDocument;
// Get new values. Must store in an array because the order that we add each
// element to the DOM matters
var filterData = [
{
nodeName: "label",
value: this.get("label"),
},
{
nodeName: "field",
value: this.get("fields"),
},
{
nodeName: "operator",
value: this.get("operator"),
},
{
nodeName: "exclude",
value: this.get("exclude"),
},
{
nodeName: "fieldsOperator",
value: this.get("fieldsOperator"),
},
{
nodeName: "matchSubstring",
value: this.get("matchSubstring"),
},
{
nodeName: "value",
value: this.get("values"),
},
];
filterData.forEach(function (element) {
var values = element.value;
var nodeName = element.nodeName;
// Serialize the nodes with multiple occurrences
if (Array.isArray(values)) {
_.each(
values,
function (value) {
// Don't serialize empty, null, or undefined values
if (value || value === false || value === 0) {
var nodeSerialized = xmlDocument.createElement(nodeName);
$(nodeSerialized).text(value);
$objectDOM.append(nodeSerialized);
}
},
this,
);
}
// Serialize the single occurrence nodes. Don't serialize falsey or default values
else if (
(values || values === false) &&
values != this.defaults()[nodeName]
) {
var nodeSerialized = xmlDocument.createElement(nodeName);
$(nodeSerialized).text(values);
$objectDOM.append(nodeSerialized);
}
}, this);
// If this is a UIFilterType that won't be serialized into a Collection definition,
// then add extra XML nodes
if (this.get("isUIFilterType")) {
//Update the filterOptions XML DOM
filterOptionsNode = this.updateFilterOptionsDOM(filterOptionsNode);
//Add the filterOptions to the filter DOM
if (
typeof filterOptionsNode != "undefined" &&
$(filterOptionsNode).children().length
) {
$objectDOM.append(filterOptionsNode);
}
}
return $objectDOM[0];
} catch (e) {
console.error("Unable to serialize a Filter.", e);
return this.get("objectDOM") || "";
}
},
/**
* Serializes the filter options into an XML DOM and returns it
* @param {Element} [filterOptionsNode] - The XML filterOptions node to update
* @return {Element} - The updated DOM
*/
updateFilterOptionsDOM: function (filterOptionsNode) {
try {
if (
typeof filterOptionsNode == "undefined" ||
!filterOptionsNode.length
) {
var filterOptionsNode = new DOMParser().parseFromString(
"<filterOptions></filterOptions>",
"text/xml",
);
var filterOptionsNode =
$(filterOptionsNode).find("filterOptions")[0];
}
//Convert the XML node into a jQuery object
var $filterOptionsNode = $(filterOptionsNode);
//Get the first option element
var firstOptionNode = $filterOptionsNode.children("option").first();
var xmlDocument;
if (filterOptionsNode.length && filterOptionsNode[0]) {
xmlDocument = filterOptionsNode[0].ownerDocument;
}
if (!xmlDocument) {
xmlDocument = filterOptionsNode.ownerDocument;
}
if (!xmlDocument) {
xmlDocument = filterOptionsNode;
}
// Update the text value of UI nodes. The following values are for
// UIFilterOptionsType
["placeholder", "icon", "description"].forEach(function (nodeName) {
//Remove the existing node, if it exists
$filterOptionsNode.children(nodeName).remove();
// If there is a value set on the model for this attribute, then create an XML
// node for this attribute and set the text value
var value = this.get(nodeName);
if (value) {
var newNode = $(xmlDocument.createElement(nodeName)).text(value);
if (firstOptionNode.length) firstOptionNode.before(newNode);
else $filterOptionsNode.append(newNode);
}
}, this);
//If no options were serialized, then return an empty string
if (!$filterOptionsNode.children().length) {
return "";
} else {
return filterOptionsNode;
}
} catch (e) {
console.log(
"Error updating the FilterOptions DOM in a Filter model, " +
"error details: ",
e,
);
return "";
}
},
/**
* Returns true if the given value or value set on this filter is a date range query
* @param {string} value - The string to test
* @return {boolean}
*/
isDateQuery: function (value) {
if (typeof value == "undefined" && this.get("values").length == 1) {
var value = this.get("values")[0];
}
if (value) {
return /[\d|\-|:|T]*Z TO [\d|\-|:|T]*Z/.test(value);
} else {
return false;
}
},
/**
* Check whether a set of query fields contain only fields that specify latitude and/or
* longitude
* @param {string[]} [fields] A list of fields to check for coordinate fields. If not
* provided, the fields set on the model will be used.
* @returns {Boolean} Returns true if every field is a field that specifies latitude or
* longitude
* @since 2.21.0
*/
isCoordinateQuery: function (fields) {
try {
if (!fields) {
fields = this.get("fields");
}
const latitudeFields = MetacatUI.appModel.get("queryLatitudeFields");
const longitudeFields = MetacatUI.appModel.get(
"queryLongitudeFields",
);
const coordinateFields = latitudeFields.concat(longitudeFields);
return _.every(fields, function (field) {
return _.contains(coordinateFields, field);
});
} catch (e) {
console.log(
"Error checking if filter is a coordinate filter. Returning false. ",
e,
);
return false;
}
},
/**
* Check whether a set of query fields contain only fields that specify latitude
* @param {string[]} [fields] A list of fields to check for coordinate fields. If not
* provided, the fields set on the model will be used.
* @returns {Boolean} Returns true if every field is a field that specifies latitude
* @since 2.21.0
*/
isLatitudeQuery: function (fields) {
try {
if (!fields) {
fields = this.get("fields");
}
const latitudeFields = MetacatUI.appModel.get("queryLatitudeFields");
return _.every(fields, function (field) {
return _.contains(latitudeFields, field);
});
} catch (e) {
console.log(
"Error checking if filter is a latitude filter. Returning false. ",
e,
);
return false;
}
},
/**
* Check whether a set of query fields contain only fields that specify longitude
* @param {string[]} [fields] A list of fields to check for longitude fields. If not
* provided, the fields set on the model will be used.
* @returns {Boolean} Returns true if every field is a field that specifies longitude
* @since 2.21.0
*/
isLongitudeQuery: function (fields) {
try {
if (!fields) {
fields = this.get("fields");
}
const longitudeFields = MetacatUI.appModel.get(
"queryLongitudeFields",
);
return _.every(fields, function (field) {
return _.contains(longitudeFields, field);
});
} catch (error) {
console.log(
"Error checking if filter is a longitude filter. Returning false. ",
e,
);
return false;
}
},
/**
* Checks if the values set on this model are valid.
* Some of the attributes are changed during this process if they are found to be invalid,
* since there aren't any easy ways for users to fix these issues themselves in the UI.
* @return {object} - Returns a literal object with the invalid attributes and their corresponding error message
*/
validate: function () {
try {
var errors = {};
// UI filter types have
var isUIFilterType = this.get("isUIFilterType");
//---Validate fields----
var fields = this.get("fields");
//All fields should be strings
var nonStrings = _.filter(fields, function (field) {
return typeof field != "string" || !field.trim().length;
});
if (nonStrings.length) {
//Remove the nonstrings from the model, rather than returning an error
this.set("fields", _.without(fields, nonStrings));
}
//If there are no fields, set an error message
if (!this.get("fields").length) {
errors.fields = "Filters should have at least one search field.";
}
//---Validate values----
var values = this.get("values");
// All values should be strings, booleans, numbers, or dates
var invalidValues = _.filter(values, function (value) {
//Empty strings are invalid
if (typeof value == "string" && !value.trim().length) {
return true;
}
//Non-empty strings, booleans, numbers, or dates are valid
else if (
typeof value == "string" ||
typeof value == "boolean" ||
typeof value == "number" ||
Date.prototype.isPrototypeOf(value)
) {
return false;
}
});
if (invalidValues.length) {
//Remove the invalid values from the model, rather than returning an error
this.set("values", _.without(values, invalidValues));
}
//If there are no values, and this isn't a custom search filter, set an error
//message.
if (!isUIFilterType && !this.get("values").length) {
errors.values = "Filters should include at least one search term.";
}
//---Validate operators ----
//The operator must be either AND or OR
["operator", "fieldsOperator"].forEach(function (op) {
if (!["AND", "OR"].includes(this.get(op))) {
//Reset the value to the default rather than return an error
this.set(op, this.defaults()[op]);
}
}, this);
//---Validate exclude and matchSubstring----
//Exclude should always be a boolean
if (typeof this.get("exclude") != "boolean") {
//Reset the value to the default rather than return an error
this.set("exclude", this.defaults().exclude);
}
//matchSubstring should always be a boolean
if (typeof this.get("matchSubstring") != "boolean") {
//Reset the value to the default rather than return an error
this.set("matchSubstring", this.defaults().matchSubstring);
}
//---Validate label, placeholder, icon, and description----
var textAttributes = ["label", "placeholder", "icon", "description"];
//These fields should be strings
_.each(
textAttributes,
function (attr) {
if (typeof this.get(attr) != "string") {
//Reset the value to the default rather than return an error
this.set(attr, this.defaults()[attr]);
}
},
this,
);
if (Object.keys(errors).length) return errors;
else {
return;
}
} catch (e) {
console.error(e);
}
},
},
);
return FilterModel;
});