flectra/addons/web/static/src/js/core/domain.js
2018-01-16 02:34:37 -08:00

273 lines
10 KiB
JavaScript

flectra.define("web.Domain", function (require) {
"use strict";
var collections = require("web.collections");
var pyeval = require("web.pyeval");
/**
* The Domain Class allows to work with a domain as a tree and provides tools
* to manipulate array and string representations of domains.
*/
var Domain = collections.Tree.extend({
/**
* @constructor
* @param {string|Array|boolean|undefined} domain
* The given domain can be:
* * a string representation of the Python prefix-array
* representation of the domain.
* * a JS prefix-array representation of the domain.
* * a boolean where the "true" domain match all records and the
* "false" domain does not match any records.
* * undefined, considered as the false boolean.
* * a number, considered as true except 0 considered as false.
* @param {Object} [evalContext] - in case the given domain is a string, an
* evaluation context might be needed
*/
init: function (domain, evalContext) {
this._super.apply(this, arguments);
if (_.isArray(domain) || _.isString(domain)) {
this._parse(this.normalizeArray(_.clone(this.stringToArray(domain, evalContext))));
} else {
this._data = !!domain;
}
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Evaluates the domain with a set of values.
*
* @param {Object} values - a mapping {fieldName -> fieldValue} (note: all
* the fields used in the domain should be given a
* value otherwise the computation will break)
* @returns {boolean}
*/
compute: function (values) {
if (this._data === true || this._data === false) {
// The domain is a always-true or a always-false domain
return this._data;
} else if (_.isArray(this._data)) {
// The domain is a [name, operator, value] entity
// First check if we have the field value in the field values set
// and if the first part of the domain contains 'parent.field'
// get the value from the parent record.
var isParentField = false;
var fieldName = this._data[0];
// We split the domain first part and check if it's a match
// for the syntax 'parent.field'.
var parentField = this._data[0].split('.');
if ('parent' in values && parentField.length === 2) {
fieldName = parentField[1];
isParentField = parentField[0] === 'parent' &&
fieldName in values.parent;
}
if (!(this._data[0] in values) && !(isParentField)) {
throw new Error(_.str.sprintf(
"Unknown field %s in domain",
this._data[0]
));
}
var fieldValue;
if (!isParentField) {
fieldValue = values[fieldName];
} else {
fieldValue = values.parent[fieldName];
}
switch (this._data[1]) {
case "=":
case "==":
return _.isEqual(fieldValue, this._data[2]);
case "!=":
case "<>":
return !_.isEqual(fieldValue, this._data[2]);
case "<":
return (fieldValue < this._data[2]);
case ">":
return (fieldValue > this._data[2]);
case "<=":
return (fieldValue <= this._data[2]);
case ">=":
return (fieldValue >= this._data[2]);
case "in":
return _.contains(
_.isArray(this._data[2]) ? this._data[2] : [this._data[2]],
fieldValue
);
case "not in":
return !_.contains(
_.isArray(this._data[2]) ? this._data[2] : [this._data[2]],
fieldValue
);
case "like":
return (fieldValue.toLowerCase().indexOf(this._data[2].toLowerCase()) >= 0);
case "ilike":
return (fieldValue.indexOf(this._data[2]) >= 0);
default:
throw new Error(_.str.sprintf(
"Domain %s uses an unsupported operator",
this._data
));
}
} else { // The domain is a set of [name, operator, value] entitie(s)
switch (this._data) {
case "&":
return _.every(this._children, function (child) {
return child.compute(values);
});
case "|":
return _.some(this._children, function (child) {
return child.compute(values);
});
case "!":
return !this._children[0].compute(values);
}
}
},
/**
* Return the JS prefix-array representation of this domain. Note that all
* domains that use the "false" domain cannot be represented as such.
*
* @returns {Array} JS prefix-array representation of this domain
*/
toArray: function () {
if (this._data === false) {
throw new Error("'false' domain cannot be converted to array");
} else if (this._data === true) {
return [];
} else {
var arr = [this._data];
return arr.concat.apply(arr, _.map(this._children, function (child) {
return child.toArray();
}));
}
},
/**
* @returns {string} representation of the Python prefix-array
* representation of the domain
*/
toString: function () {
return Domain.prototype.arrayToString(this.toArray());
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Initializes the tree representation of the domain according to its given
* JS prefix-array representation. Note: the given array is considered
* already normalized.
*
* @private
* @param {Array} domain - normalized JS prefix-array representation of
* the domain
*/
_parse: function (domain) {
this._data = (domain.length === 0 ? true : domain[0]);
if (domain.length <= 1) return;
var expected = 1;
for (var i = 1 ; i < domain.length ; i++) {
if (domain[i] === "&" || domain[i] === "|") {
expected++;
} else if (domain[i] !== "!") {
expected--;
}
if (!expected) {
i++;
this._addSubdomain(domain.slice(1, i));
this._addSubdomain(domain.slice(i));
break;
}
}
},
/**
* Adds a domain as a child (e.g. if the current domain is ["|", A, B],
* using this method with a ["&", C, D] domain will result in a
* ["|", "|", A, B, "&", C, D]).
* Note: the internal tree representation is automatically simplified.
*
* @param {Array} domain - normalized JS prefix-array representation of a
* domain to add
*/
_addSubdomain: function (domain) {
if (!domain.length) return;
var subdomain = new Domain(domain);
if (!subdomain._children.length || subdomain._data !== this._data) {
this._children.push(subdomain);
} else {
var self = this;
_.each(subdomain._children, function (childDomain) {
self._children.push(childDomain);
});
}
},
//--------------------------------------------------------------------------
// Static
//--------------------------------------------------------------------------
/**
* Converts JS prefix-array representation of a domain to a string
* representation of the Python prefix-array representation of this domain.
*
* @static
* @param {Array|string} domain
* @returns {string}
*/
arrayToString: function (domain) {
if (_.isString(domain)) return domain;
return JSON.stringify(domain || [])
.replace(/null/g, "None")
.replace(/false/g, "False")
.replace(/true/g, "True");
},
/**
* Converts a string representation of the Python prefix-array
* representation of a domain to a JS prefix-array representation of this
* domain.
*
* @static
* @param {string|Array} domain
* @param {Object} [evalContext]
* @returns {Array}
*/
stringToArray: function (domain, evalContext) {
if (!_.isString(domain)) return _.clone(domain);
return pyeval.eval("domain", domain || "[]", evalContext);
},
/**
* Makes implicit "&" operators explicit in the given JS prefix-array
* representation of domain (e.g [A, B] -> ["&", A, B])
*
* @static
* @param {Array} domain - the JS prefix-array representation of the domain
* to normalize (! will be normalized in-place)
* @returns {Array} the normalized JS prefix-array representation of the
* given domain
*/
normalizeArray: function (domain) {
var expected = 1;
_.each(domain, function (item) {
if (item === "&" || item === "|") {
expected++;
} else if (item !== "!") {
expected--;
}
});
if (expected < 0) {
domain.unshift.apply(domain, _.times(Math.abs(expected), _.constant("&")));
}
return domain;
},
});
return Domain;
});