diff options
| author | Adrien Dieudonne <adr@odoo.com> | 2018-07-06 14:22:58 +0200 |
|---|---|---|
| committer | Adrien Dieudonne <adr@odoo.com> | 2018-07-06 14:22:58 +0200 |
| commit | 3ebff1f4721dd4a9057db4d26162c57c8506700a (patch) | |
| tree | e3b02fcc528dcc3a0ae50b55a5d3a1d4708ab992 /addons/web/static/src/js | |
| parent | f9a68d5e778da0ff4e1f1a0936b51b98a822a15a (diff) | |
| parent | 11b7857af63a35e81daa495bcb3522b3915f32ca (diff) | |
[MERGE] *: Easy date filters
This commit allow to create easily a range of periods (related to a field
of date or datetime type) to select in the filters menu.
Two new attributes on elements with tag 'filter' are now usable
in combination in a search view arch:
- date: string.
A valid field name with associated type date or datetime
- default_period: string (optional).
To choose in the following list:
- 'today'
- 'this_week'
- 'this_month' (default)
- 'this_quarter'
- 'this_year'
- 'yesterday'
- 'last_week'
- 'last_month'
- 'last_quarter'
- 'last_year'
- 'last_7_days'
- 'last_30_days'
- 'last_365_days'
Example:
<filter string="Creation Date" name="period_generator" date="create_date" default_period="this_quarter"/>
Note: the attribute 'domain' can not be used for such filters.
The generated domains are dynamic and can be saved as such
via the favorites menu.
task-10532
Diffstat (limited to 'addons/web/static/src/js')
19 files changed, 1029 insertions, 1293 deletions
diff --git a/addons/web/static/src/js/_deprecated/data.js b/addons/web/static/src/js/_deprecated/data.js index eb70f330076..fd19ff61af7 100644 --- a/addons/web/static/src/js/_deprecated/data.js +++ b/addons/web/static/src/js/_deprecated/data.js @@ -7,7 +7,7 @@ var concurrency = require('web.concurrency'); var mixins = require('web.mixins'); var session = require('web.session'); var translation = require('web.translation'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var _t = translation._t; @@ -85,9 +85,9 @@ var Query = Class.extend({ return session.rpc('/web/dataset/search_read', { model: this._model.name, fields: this._fields || false, - domain: pyeval.eval('domains', + domain: pyUtils.eval('domains', [this._model.domain(this._filter)]), - context: pyeval.eval('contexts', + context: pyUtils.eval('contexts', [this._model.context(this._context)]), offset: this._offset, limit: this._limit, @@ -138,7 +138,7 @@ var Query = Class.extend({ * @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null} */ group_by: function (grouping) { - var ctx = pyeval.eval( + var ctx = pyUtils.eval( 'context', this._model.context(this._context)); // undefined passed in explicitly (!) @@ -609,7 +609,7 @@ var DataSet = Class.extend(mixins.PropertiesMixin, { return session.rpc('/web/dataset/resequence', { model: this.model, ids: ids, - context: pyeval.eval( + context: pyUtils.eval( 'context', this.get_context(options.context)), }).then(function (results) { return results; @@ -797,7 +797,7 @@ var Model = Class.extend({ * Call a method (over RPC) on the bound OpenERP model. * * @param {String} method name of the method to call - * @param {Array} [args] positional arguments + * @param {Array} [args] positipyEvalonal arguments * @param {Object} [kwargs] keyword arguments * @param {Object} [options] additional options for the rpc() method * @returns {jQuery.Deferred<>} call result @@ -810,7 +810,7 @@ var Model = Class.extend({ kwargs = args; args = []; } - pyeval.ensure_evaluated(args, kwargs); + pyUtils.ensure_evaluated(args, kwargs); var call_kw = '/web/dataset/call_kw/' + this.name + '/' + method; return session.rpc(call_kw, { model: this.name, @@ -820,7 +820,7 @@ var Model = Class.extend({ }, options); }, call_button: function (method, args) { - pyeval.ensure_evaluated(args, {}); + pyUtils.ensure_evaluated(args, {}); return session.rpc('/web/dataset/call_button', { model: this.name, method: method, diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js index f98447c8983..2eb698b5424 100644 --- a/addons/web/static/src/js/chrome/action_manager.js +++ b/addons/web/static/src/js/chrome/action_manager.js @@ -19,7 +19,7 @@ var core = require('web.core'); var Dialog = require('web.Dialog'); var dom = require('web.dom'); var framework = require('web.framework'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var Widget = require('web.Widget'); var _t = core._t; @@ -781,9 +781,9 @@ var ActionManager = Widget.extend({ _preprocessAction: function (action, options) { // ensure that the context and domain are evaluated var context = new Context(this.userContext, options.additional_context, action.context); - action.context = pyeval.eval('context', context); + action.context = pyUtils.eval('context', context); if (action.domain) { - action.domain = pyeval.eval('domain', action.domain, action.context); + action.domain = pyUtils.eval('domain', action.domain, action.context); } action._originalAction = JSON.stringify(action); diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index e040096f60b..0b18b04172a 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -11,7 +11,7 @@ var config = require('web.config'); var Context = require('web.Context'); var core = require('web.core'); var data = require('web.data'); // this will be removed at some point -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var SearchView = require('web.SearchView'); var view_registry = require('web.view_registry'); @@ -522,7 +522,7 @@ ActionManager.include({ var domains = searchData.domains; var groupbys = searchData.groupbys; var action_context = action.context || {}; - var results = pyeval.eval_domains_and_contexts({ + var results = pyUtils.eval_domains_and_contexts({ domains: [action.domain || []].concat(domains || []), contexts: [action_context].concat(contexts || []), group_by_seq: groupbys || [], @@ -703,7 +703,7 @@ ActionManager.include({ var actionData = ev.data.action_data; var env = ev.data.env; var context = new Context(env.context, actionData.context || {}); - var recordID = env.currentID || null; // pyeval handles null value, not undefined + var recordID = env.currentID || null; // pyUtils handles null value, not undefined var def; // determine the action to execute according to the actionData @@ -733,7 +733,7 @@ ActionManager.include({ }); } else if (actionData.type === 'action') { // execute a given action, so load it first - def = this._loadAction(actionData.name, _.extend(pyeval.eval('context', context), { + def = this._loadAction(actionData.name, _.extend(pyUtils.eval('context', context), { active_model: env.model, active_ids: env.resIDs, active_id: recordID, @@ -751,7 +751,7 @@ ActionManager.include({ // code below handles the first case i.e 'effect' attribute on button. var effect = false; if (actionData.effect) { - effect = pyeval.py_eval(actionData.effect); + effect = pyUtils.py_eval(actionData.effect); } if (action && action.constructor === Object) { diff --git a/addons/web/static/src/js/chrome/sidebar.js b/addons/web/static/src/js/chrome/sidebar.js index bf9e3668c35..47d598aaeb8 100644 --- a/addons/web/static/src/js/chrome/sidebar.js +++ b/addons/web/static/src/js/chrome/sidebar.js @@ -3,7 +3,7 @@ odoo.define('web.Sidebar', function (require) { var Context = require('web.Context'); var core = require('web.core'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var Widget = require('web.Widget'); var QWeb = core.qweb; @@ -146,7 +146,7 @@ var Sidebar = Widget.extend({ activeIdsContext.active_domain = env.domain; } - var context = pyeval.eval('context', new Context(env.context, activeIdsContext)); + var context = pyUtils.eval('context', new Context(env.context, activeIdsContext)); self._rpc({ route: '/web/action/load', params: { diff --git a/addons/web/static/src/js/core/context.js b/addons/web/static/src/js/core/context.js index 53254bd718a..a7ce801708d 100644 --- a/addons/web/static/src/js/core/context.js +++ b/addons/web/static/src/js/core/context.js @@ -2,7 +2,7 @@ odoo.define('web.Context', function (require) { "use strict"; var Class = require('web.Class'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var Context = Class.extend({ init: function () { @@ -24,7 +24,7 @@ var Context = Class.extend({ return this; }, eval: function () { - return pyeval.eval('context', this); + return pyUtils.eval('context', this); }, /** * Set the evaluation context to be used when we actually eval. diff --git a/addons/web/static/src/js/core/domain.js b/addons/web/static/src/js/core/domain.js index 8ce570b8605..e8f089dca76 100644 --- a/addons/web/static/src/js/core/domain.js +++ b/addons/web/static/src/js/core/domain.js @@ -2,7 +2,7 @@ odoo.define("web.Domain", function (require) { "use strict"; var collections = require("web.collections"); -var pyeval = require("web.pyeval"); +var pyUtils = require("web.py_utils"); /** * The Domain Class allows to work with a domain as a tree and provides tools @@ -228,6 +228,86 @@ var Domain = collections.Tree.extend({ .replace(/false/g, "False") .replace(/true/g, "True"); }, + /* + * @param {string} fieldName + * @param {string} period + * @param {string} type ('date' or 'datetime') + * @param {boolean} forTooltip indicates if domain is used to print a tooltip + */ + constructDomain: function (fieldName, period, type, forTooltip) { + var leftBoundaryParams, rightBoundaryParams; + var t = forTooltip || false; + function makeInterval () { + if (type === 'date') { + return "['&'," + + "('" + fieldName + "', '>=', (context_today() + relativedelta(" + leftBoundaryParams + ")).strftime('%Y-%m-%d'))," + + "('" + fieldName + "', '<', (context_today() + relativedelta(" + rightBoundaryParams + ")).strftime('%Y-%m-%d'))"+ + "]"; + } + else { + return "['&'," + + "('" + fieldName + "', '>=', " + + "(datetime.datetime.combine(context_today() + relativedelta(" + leftBoundaryParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))," + + "('" + fieldName + "', '<', " + + "(datetime.datetime.combine(context_today() + relativedelta(" + rightBoundaryParams + "), datetime.time(0,0,0)).to_utc()).strftime('%Y-%m-%d %H:%M:%S'))"+ + "]"; + } + } + switch (period) { + case 'today': + leftBoundaryParams = ""; + rightBoundaryParams = t ? "" : "days=1"; + return makeInterval(); + case 'this_week': + leftBoundaryParams = "weeks=-1, days=1, weekday=0"; + rightBoundaryParams = t ? "weekday=6" : "days=1, weekday=0"; + return makeInterval(); + case 'this_month': + leftBoundaryParams = "day=1"; + rightBoundaryParams = (t ? "days=-1, " : "") + "day=1, months=1"; + return makeInterval(); + case 'this_quarter': + leftBoundaryParams = "months= - (context_today().month - 1) % 3, day=1"; + rightBoundaryParams = (t ? "days=-1, " : "") + "months= 3 - (context_today().month -1 ) % 3, day=1"; + return makeInterval(); + case 'this_year': + leftBoundaryParams = "month=1, day=1"; + rightBoundaryParams = (t ? "days=-1, " : "") + "month=1, day=1, years=1"; + return makeInterval(); + case 'yesterday': + leftBoundaryParams = "days=-1"; + rightBoundaryParams = (t ? "days=-1" : ""); + return makeInterval(); + case 'last_week': + leftBoundaryParams = "weeks=-2, days=1, weekday=0"; + rightBoundaryParams = t ? "weeks=-1, weekday=6" : "weeks=-1, days=1, weekday=0"; + return makeInterval(); + case 'last_month': + leftBoundaryParams = "months=-1, day=1"; + rightBoundaryParams = (t ? "days=-1 , " : "") + "day=1"; + return makeInterval(); + case 'last_quarter': + leftBoundaryParams = "months= - 3 - (context_today().month - 1) % 3, day=1"; + rightBoundaryParams = (t ? "days=-1 , " : "") + "months= - (context_today().month - 1) % 3, day=1"; + return makeInterval(); + case 'last_year': + leftBoundaryParams = "month=1, day=1, years=-1"; + rightBoundaryParams = (t ? "days=-1 , " : "") + "month=1, day=1"; + return makeInterval(); + case 'last_7_days': + leftBoundaryParams = "days=-7"; + rightBoundaryParams = t ? "days=-1" : ""; + return makeInterval(); + case 'last_30_days': + leftBoundaryParams = "days=-30"; + rightBoundaryParams = t ? "days=-1" : ""; + return makeInterval(); + case 'last_365_days': + leftBoundaryParams = "days=-365"; + rightBoundaryParams = t ? "days=-1" : ""; + return makeInterval(); + } + }, /** * Converts a string representation of the Python prefix-array * representation of a domain to a JS prefix-array representation of this @@ -240,7 +320,7 @@ var Domain = collections.Tree.extend({ */ stringToArray: function (domain, evalContext) { if (!_.isString(domain)) return _.clone(domain); - return pyeval.eval("domain", domain ? domain.replace(/%%/g, '%') : "[]", evalContext); + return pyUtils.eval("domain", domain ? domain.replace(/%%/g, '%') : "[]", evalContext); }, /** * Makes implicit "&" operators explicit in the given JS prefix-array diff --git a/addons/web/static/src/js/core/py_utils.js b/addons/web/static/src/js/core/py_utils.js new file mode 100644 index 00000000000..83e24b207c6 --- /dev/null +++ b/addons/web/static/src/js/core/py_utils.js @@ -0,0 +1,536 @@ +odoo.define('web.py_utils', function (require) { +"use strict"; + +var core = require('web.core'); + +var _t = core._t; +var py = window.py; // to silence linters + +// recursively wraps JS objects passed into the context to attributedicts +// which jsonify back to JS objects +function wrap(value) { + if (value === null) { return py.None; } + + switch (typeof value) { + case 'undefined': throw new Error("No conversion for undefined"); + case 'boolean': return py.bool.fromJSON(value); + case 'number': return py.float.fromJSON(value); + case 'string': return py.str.fromJSON(value); + } + + switch(value.constructor) { + case Object: return wrapping_dict.fromJSON(value); + case Array: return wrapping_list.fromJSON(value); + } + + throw new Error("ValueError: unable to wrap " + value); +} + +var wrapping_dict = py.type('wrapping_dict', null, { + __init__: function () { + this._store = {}; + }, + __getitem__: function (key) { + var k = key.toJSON(); + if (!(k in this._store)) { + throw new Error("KeyError: '" + k + "'"); + } + return wrap(this._store[k]); + }, + __getattr__: function (key) { + return this.__getitem__(py.str.fromJSON(key)); + }, + __len__: function () { + return Object.keys(this._store).length; + }, + __nonzero__: function () { + return py.PY_size(this) > 0 ? py.True : py.False; + }, + get: function () { + var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]); + + if (!(args.k.toJSON() in this._store)) { return args.d; } + return this.__getitem__(args.k); + }, + fromJSON: function (d) { + var instance = py.PY_call(wrapping_dict); + instance._store = d; + return instance; + }, + toJSON: function () { + return this._store; + }, +}); + +var wrapping_list = py.type('wrapping_list', null, { + __init__: function () { + this._store = []; + }, + __getitem__: function (index) { + return wrap(this._store[index.toJSON()]); + }, + __len__: function () { + return this._store.length; + }, + __nonzero__: function () { + return py.PY_size(this) > 0 ? py.True : py.False; + }, + fromJSON: function (ar) { + var instance = py.PY_call(wrapping_list); + instance._store = ar; + return instance; + }, + toJSON: function () { + return this._store; + }, +}); + +function wrap_context(context) { + for (var k in context) { + if (!context.hasOwnProperty(k)) { continue; } + var val = context[k]; + + if (val === null) { continue; } + if (val.constructor === Array) { + context[k] = wrapping_list.fromJSON(val); + } else if (val.constructor === Object + && !py.PY_isInstance(val, py.object)) { + context[k] = wrapping_dict.fromJSON(val); + } + } + return context; +} + +function eval_contexts(contexts, evaluation_context) { + evaluation_context = _.extend(pycontext(), evaluation_context || {}); + return _(contexts).reduce(function (result_context, ctx) { + // __eval_context evaluations can lead to some of `contexts`'s + // values being null, skip them as well as empty contexts + if (_.isEmpty(ctx)) { return result_context; } + if (_.isString(ctx)) { + // wrap raw strings in context + ctx = { __ref: 'context', __debug: ctx }; + } + var evaluated = ctx; + switch(ctx.__ref) { + case 'context': + evaluation_context.context = evaluation_context; + evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context)); + break; + case 'compound_context': + var eval_context = eval_contexts([ctx.__eval_context]); + evaluated = eval_contexts( + ctx.__contexts, _.extend({}, evaluation_context, eval_context)); + break; + } + // add newly evaluated context to evaluation context for following + // siblings + _.extend(evaluation_context, evaluated); + return _.extend(result_context, evaluated); + }, {}); +} + +function eval_domains(domains, evaluation_context) { + evaluation_context = _.extend(pycontext(), evaluation_context || {}); + var result_domain = []; + // Normalize only if the first domain is the array ["|"] or ["!"] + var need_normalization = ( + domains && + domains.length > 0 && + domains[0].length === 1 && + (domains[0][0] === "|" || domains[0][0] === "!") + ); + _(domains).each(function (domain) { + if (_.isString(domain)) { + // wrap raw strings in domain + domain = { __ref: 'domain', __debug: domain }; + } + var domain_array_to_combine; + switch(domain.__ref) { + case 'domain': + evaluation_context.context = evaluation_context; + domain_array_to_combine = py.eval(domain.__debug, wrap_context(evaluation_context)); + break; + default: + domain_array_to_combine = domain; + } + if (need_normalization) { + domain_array_to_combine = get_normalized_domain(domain_array_to_combine); + } + result_domain.push.apply(result_domain, domain_array_to_combine); + }); + return result_domain; +} + +/** + * Returns a normalized copy of the given domain array. Normalization is + * is making the implicit "&" at the start of the domain explicit, e.g. + * [A, B, C] would become ["&", "&", A, B, C]. + * + * @param {Array} domain_array + * @returns {Array} normalized copy of the given array + */ +function get_normalized_domain(domain_array) { + var expected = 1; // Holds the number of expected domain expressions + _.each(domain_array, function (item) { + if (item === "&" || item === "|") { + expected++; + } else if (item !== "!") { + expected--; + } + }); + var new_explicit_ands = _.times(-expected, _.constant("&")); + return new_explicit_ands.concat(domain_array); +} + +function eval_groupbys(contexts, evaluation_context) { + evaluation_context = _.extend(pycontext(), evaluation_context || {}); + var result_group = []; + _(contexts).each(function (ctx) { + if (_.isString(ctx)) { + // wrap raw strings in context + ctx = { __ref: 'context', __debug: ctx }; + } + var group; + var evaluated = ctx; + switch(ctx.__ref) { + case 'context': + evaluation_context.context = evaluation_context; + evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context)); + break; + case 'compound_context': + var eval_context = eval_contexts([ctx.__eval_context]); + evaluated = eval_contexts( + ctx.__contexts, _.extend({}, evaluation_context, eval_context)); + break; + } + group = evaluated.group_by; + if (!group) { return; } + if (typeof group === 'string') { + result_group.push(group); + } else if (group instanceof Array) { + result_group.push.apply(result_group, group); + } else { + throw new Error('Got invalid groupby {{' + + JSON.stringify(group) + '}}'); + } + _.extend(evaluation_context, evaluated); + }); + return result_group; +} + +/** + * Returns the current local date, which means the date on the client (which can be different + * compared to the date of the server). + * + * @return {datetime.date} + */ +function context_today() { + var d = new Date(); + return py.PY_call( + py.extras.datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]); +} + +/** + * Returns a timedelta object which represents the timezone offset between the + * local timezone and the UTC time. + * + * This is very useful to generate datetime strings which are 'timezone' + * dependant. For example, we can now write this to generate the correct + * datetime string representing "this morning in the user timezone": + * + * "datetime.datetime.now().replace(hour=0,minute=0,second=0) + tz_offset()).strftime('%Y-%m-%d %H:%M:%S')" + * @returns {datetime.timedelta} + */ +function tz_offset() { + var offset= new Date().getTimezoneOffset(); + var kwargs = {minutes: py.float.fromJSON(offset)}; + return py.PY_call(py.extras.datetime.timedelta,[],kwargs); +} + + +function pycontext() { + return { + datetime: py.extras.datetime, + context_today: context_today, + time: py.extras.time, + relativedelta: py.extras.relativedelta, + current_date: py.PY_call( + py.extras.time.strftime, [py.str.fromJSON('%Y-%m-%d')]), + tz_offset: tz_offset, + }; +} + +/** + * @param {String} type "domains", "contexts" or "groupbys" + * @param {Array} object domains or contexts to evaluate + * @param {Object} [context] evaluation context + */ +function pyeval(type, object, context) { + context = _.extend(pycontext(), context || {}); + + //noinspection FallthroughInSwitchStatementJS + switch(type) { + case 'context': + case 'contexts': + if (type === 'context') { + object = [object]; + } + return eval_contexts(object, context); + case 'domain': + case 'domains': + if (type === 'domain') + object = [object]; + return eval_domains(object, context); + case 'groupbys': + return eval_groupbys(object, context); + } + throw new Error("Unknow evaluation type " + type); +} + +function eval_arg(arg) { + if (typeof arg !== 'object' || !arg.__ref) { return arg; } + switch(arg.__ref) { + case 'domain': + return pyeval('domains', [arg]); + case 'context': case 'compound_context': + return pyeval('contexts', [arg]); + default: + throw new Error(_t("Unknown nonliteral type ") + ' ' + arg.__ref); + } +} + +/** + * If args or kwargs are unevaluated contexts or domains (compound or not), + * evaluated them in-place. + * + * Potentially mutates both parameters. + * + * @param args + * @param kwargs + */ +function ensure_evaluated(args, kwargs) { + for (var i=0; i<args.length; ++i) { + args[i] = eval_arg(args[i]); + } + for (var k in kwargs) { + if (!kwargs.hasOwnProperty(k)) { continue; } + kwargs[k] = eval_arg(kwargs[k]); + } +} + +function eval_domains_and_contexts(source) { + // see Session.eval_context in Python + return { + context: pyeval('contexts', source.contexts || [], source.eval_context), + domain: pyeval('domains', source.domains, source.eval_context), + group_by: pyeval('groupbys', source.group_by_seq || [], source.eval_context), + }; +} + +function py_eval(expr, context) { + return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null})); +} + +/** + * Assemble domains into a single domains using an 'OR' or an 'AND' operator. + * + * Note: - this function does not evaluate anything inside the domain. This is + * actually quite critical because this allows the manipulation of unevaluated + * (dynamic) domains. + * - this function gives a normalized domain as result, + * - applied on a list of length 1, it returns the domain normalized. + * + * @param {string[]} domains list of string representing domains + * @param {"AND" | "OR"} operator used to combine domains (default "AND") + * @returns {string} normalized domain + */ +function assembleDomains(domains, operator) { + var ASTs = domains.map(_getPyJSAST); + if (operator === "OR") { + operator = py.tokenize("'|'")[0]; + } else { + operator = py.tokenize("'&'")[0]; + } + var result = _getPyJSAST("[]"); + var normalizedDomains = ASTs + .filter(function (AST) { + return AST.first.length > 0; + }) + .map(_normalizeDomainAST); + if (normalizedDomains.length > 0) { + result.first = normalizedDomains.reduce(function (acc, ast) { + return acc.concat(ast.first); + }, + _.times(normalizedDomains.length - 1, _.constant(operator)) + ); + } + return _formatAST(result); +} +/** + * Normalize a domain via its string representation. + * + * Note: this function does not evaluate anything inside the domain. This is + * actually quite critical because this allows the manipulation of unevaluated + * (dynamic) domains. + * + * @param {string} domain string representing a domain + * @returns {string} normalized domain + */ +function normalizeDomain (domain) { + return _formatAST(_normalizeDomainAST(_getPyJSAST(domain))); +} + +//-------------------------------------------------------------------------- +// Private +//-------------------------------------------------------------------------- + +// Binding power for prefix operator is not accessible in the AST generated by +// py.js, so we have to hardcode some values here +var BINDING_POWERS = { + or: 30, + and: 40, + not: 50, +}; + +/** + * @private + * Convert a python AST (generated by py.js) to a string form, which should + * represent the same AST. + * + * @param {Object} ast a valid AST obtained by py.js, which represent a python + * expression + * @param {integer} [lbp=0] a binding power. This is necessary to be able to + * format sub expressions: the + sub expression in "3 * (a + 2)" should be + * formatted with parenthesis, because its binding power is lower than the + * binding power of *. + * @returns {string} + */ +function _formatAST(ast, lbp) { + lbp = lbp || 0; + switch (ast.id) { + // basic values + case "(number)": + return String(ast.value); + case "(string)": + return "'" + ast.value + "'"; + case "(constant)": + return ast.value; + case "(name)": + return ast.value; + case "[": + if (ast.second) { + // read a value in a dictionary: d['a'] + return _formatAST(ast.first) + '[' + _formatAST(ast.second) + ']'; + } else { + // list: [1, 2] + var values = ast.first.map(_formatAST); + return '[' + values.join(', ') + ']'; + } + case "{": + var keyValues = ast.first.map(function (kv) { + return _formatAST(kv[0]) + ': ' + _formatAST(kv[1]); + }); + return '{' + keyValues.join(', ') + '}'; + + // relations + case "=": + return _formatAST(ast.first) + ' ' + ast.id + ' ' + _formatAST(ast.second); + // operators + case "-": + case "+": + case "~": + case "*": + case "**": + case "%": + case "//": + case "and": + case "or": + if (ast.second) { + // infix + var r = _formatAST(ast.first, ast.lbp) + ' ' + ast.id + ' ' + _formatAST(ast.second, ast.lbp); + if (ast.lbp < lbp) { + r = '(' + r + ')'; + } + return r; + } + // prefix + // real lbp is not accessible, it is inside a closure + var actualBP = BINDING_POWERS[ast.id] || 130; + return ast.id + _formatAST(ast.first, actualBP); + case ".": + return _formatAST(ast.first, ast.lbp) + '.' + _formatAST(ast.second); + case "not": + return "not " + _formatAST(ast.first); + case "(comparator)": + var operator = ast.operators[0]; + return _formatAST(ast.expressions[0]) + ' ' + operator + ' ' + _formatAST(ast.expressions[1]); + + // function call + case "(": + if (ast.second) { + // this is a function call: f(a, b) + return _formatAST(ast.first) + '(' + ast.second.map(_formatAST).join(', ') + ')'; + } else { + // this is a tuple + return '(' + ast.first.map(_formatAST).join(', ') + ')'; + } + } + throw new Error("Unimplemented python construct"); +} + +/** + * @private + * Get the PyJs AST representing a domain starting from is string representation + * + * @param {string} domain string representing a domain + * @returns {PyJS AST} PyJS AST representation of domain + */ +function _getPyJSAST(domain) { + return py.parse(py.tokenize(domain)); +} + +/** + * @private + * + * Normalize a domain, at the level of the AST. + * + * Note: this function does not evaluate anything inside the domain. This is + * actually quite critical because this allows the manipulation of unevaluated + * (dynamic) domains. + * + * @param {PyJS AST} domain valid AST representing a domain + * @returns {PyJS AST} normalized domain AST + */ +function _normalizeDomainAST(domain) { + var expected = 1; + for (var i = 0; i < domain.first.length; i++) { + var value = domain.first[i].value; + if (value === '&' || value === '|') { + expected++; + } else if (value !== '!') { + expected--; + } + } + var andOperator = py.tokenize("'&'")[0]; + + if (expected < 0) { + domain.first.unshift.apply(domain.first, _.times(Math.abs(expected), _.constant(andOperator))); + } + + return domain; +} + +return { + context: pycontext, + ensure_evaluated: ensure_evaluated, + eval: pyeval, + eval_domains_and_contexts: eval_domains_and_contexts, + py_eval: py_eval, + normalizeDomain: normalizeDomain, + assembleDomains: assembleDomains, + _getPyJSAST: _getPyJSAST, + _formatAST: _formatAST, + _normalizeDomainAST: _normalizeDomainAST, +}; +}); diff --git a/addons/web/static/src/js/core/pyeval.js b/addons/web/static/src/js/core/pyeval.js deleted file mode 100644 index ee4ac697fd9..00000000000 --- a/addons/web/static/src/js/core/pyeval.js +++ /dev/null @@ -1,1046 +0,0 @@ -odoo.define('web.pyeval', function (require) { -"use strict"; - -var core = require('web.core'); -var utils = require('web.utils'); - -var _t = core._t; -var py = window.py; // to silence linters -/* - * py.js helpers and setup - */ - -var obj = function () {}; -obj.prototype = py.object; -var asJS = function (arg) { - if (arg instanceof obj) { - return arg.toJSON(); - } - return arg; -}; - -var datetime = py.PY_call(py.object); - -var zero = py.float.fromJSON(0); - -// Port from pypy/lib_pypy/datetime.py -var DAYS_IN_MONTH = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; -var DAYS_BEFORE_MONTH = [null]; -var dbm = 0; - -for (var i=1; i<DAYS_IN_MONTH.length; ++i) { - DAYS_BEFORE_MONTH.push(dbm); - dbm += DAYS_IN_MONTH[i]; -} - -function is_leap(year) { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); -} - -function days_before_year(year) { - var y = year - 1; - return y*365 + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400); -} - -function days_in_month(year, month) { - if (month === 2 && is_leap(year)) { - return 29; - } - return DAYS_IN_MONTH[month]; -} - -function days_before_month(year, month) { - var post_leap_feb = month > 2 && is_leap(year); - return DAYS_BEFORE_MONTH[month] + (post_leap_feb ? 1 : 0); -} - -function ymd2ord(year, month, day) { - var dim = days_in_month(year, month); - if (!(1 <= day && day <= dim)) { - throw new Error("ValueError: day must be in 1.." + dim); - } - return days_before_year(year) + - days_before_month(year, month) + - day; -} - -var DI400Y = days_before_year(401); -var DI100Y = days_before_year(101); -var DI4Y = days_before_year(5); - -function ord2ymd(n) { - --n; - var n400, n100, n4, n1, n0; - utils.divmod(n, DI400Y, function (_n400, n) { - n400 = _n400; - utils.divmod(n, DI100Y, function (_n100, n) { - n100 = _n100; - utils.divmod(n, DI4Y, function (_n4, n) { - n4 = _n4; - utils.divmod(n, 365, function (_n1, n) { - n1 = _n1; - n0 = n; - }); - }); - }); - }); - - n = n0; - var year = n400 * 400 + 1 + n100 * 100 + n4 * 4 + n1; - if (n1 == 4 || n100 == 100) { - utils.assert(n0 === 0); - return { - year: year - 1, - month: 12, - day: 31 - }; - } - - var leapyear = n1 === 3 && (n4 !== 24 || n100 == 3); - utils.assert(leapyear == is_leap(year)); - var month = (n + 50) >> 5; - var preceding = DAYS_BEFORE_MONTH[month] + ((month > 2 && leapyear) ? 1 : 0); - if (preceding > n) { - --month; - preceding -= DAYS_IN_MONTH[month] + ((month === 2 && leapyear) ? 1 : 0); - } - n -= preceding; - return { - year: year, - month: month, - day: n+1 - }; -} - -/** - * Converts the stuff passed in into a valid date, applying overflows as needed - */ -function tmxxx(year, month, day, hour, minute, second, microsecond) { - hour = hour || 0; minute = minute || 0; second = second || 0; - microsecond = microsecond || 0; - - if (microsecond < 0 || microsecond > 999999) { - utils.divmod(microsecond, 1000000, function (carry, ms) { - microsecond = ms; - second += carry; - }); - } - if (second < 0 || second > 59) { - utils.divmod(second, 60, function (carry, s) { - second = s; - minute += carry; - }); - } - if (minute < 0 || minute > 59) { - utils.divmod(minute, 60, function (carry, m) { - minute = m; - hour += carry; - }); - } - if (hour < 0 || hour > 23) { - utils.divmod(hour, 24, function (carry, h) { - hour = h; - day += carry; - }); - } - // That was easy. Now it gets muddy: the proper range for day - // can't be determined without knowing the correct month and year, - // but if day is, e.g., plus or minus a million, the current month - // and year values make no sense (and may also be out of bounds - // themselves). - // Saying 12 months == 1 year should be non-controversial. - if (month < 1 || month > 12) { - utils.divmod(month-1, 12, function (carry, m) { - month = m + 1; - year += carry; - }); - } - // Now only day can be out of bounds (year may also be out of bounds - // for a datetime object, but we don't care about that here). - // If day is out of bounds, what to do is arguable, but at least the - // method here is principled and explainable. - var dim = days_in_month(year, month); - if (day < 1 || day > dim) { - // Move day-1 days from the first of the month. First try to - // get off cheap if we're only one day out of range (adjustments - // for timezone alone can't be worse than that). - if (day === 0) { - --month; - if (month > 0) { - day = days_in_month(year, month); - } else { - --year; month=12; day=31; - } - } else if (day == dim + 1) { - ++month; - day = 1; - if (month > 12) { - month = 1; - ++year; - } - } else { - var r = ord2ymd(ymd2ord(year, month, 1) + (day - 1)); - year = r.year; - month = r.month; - day = r.day; - } - } - return { - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - }; -} - -datetime.timedelta = py.type('timedelta', null, { - __init__: function () { - var args = py.PY_parseArgs(arguments, [ - ['days', zero], ['seconds', zero], ['microseconds', zero], - ['milliseconds', zero], ['minutes', zero], ['hours', zero], - ['weeks', zero] - ]); - - var d = 0, s = 0, m = 0; - var days = args.days.toJSON() + args.weeks.toJSON() * 7; - var seconds = args.seconds.toJSON() - + args.minutes.toJSON() * 60 - + args.hours.toJSON() * 3600; - var microseconds = args.microseconds.toJSON() - + args.milliseconds.toJSON() * 1000; - - // Get rid of all fractions, and normalize s and us. - // Take a deep breath <wink>. - var daysecondsfrac = utils.modf(days, function (dayfrac, days) { - d = days; - if (dayfrac) { - return utils.modf(dayfrac * 24 * 3600, function (dsf, dsw) { - s = dsw; - return dsf; - }); - } - return 0; - }); - - var secondsfrac = utils.modf(seconds, function (sf, s) { - seconds = s; - return sf + daysecondsfrac; - }); - utils.divmod(seconds, 24*3600, function (days, seconds) { - d += days; - s += seconds; - }); - // seconds isn't referenced again before redefinition - - microseconds += secondsfrac * 1e6; - utils.divmod(microseconds, 1000000, function (seconds, microseconds) { - utils.divmod(seconds, 24*3600, function (days, seconds) { - d += days; - s += seconds; - m += Math.round(microseconds); - }); - }); - - // Carrying still possible here? - - this.days = d; - this.seconds = s; - this.microseconds = m; - }, - __str__: function () { - var hh, mm, ss; - utils.divmod(this.seconds, 60, function (m, s) { - utils.divmod(m, 60, function (h, m) { - hh = h; - mm = m; - ss = s; - }); - }); - var s = _.str.sprintf("%d:%02d:%02d", hh, mm, ss); - if (this.days) { - s = _.str.sprintf("%d day%s, %s", - this.days, - (this.days != 1 && this.days != -1) ? 's' : '', - s); - } - if (this.microseconds) { - s = _.str.sprintf("%s.%06d", s, this.microseconds); - } - return py.str.fromJSON(s); - }, - __eq__: function (other) { - if (!py.PY_isInstance(other, datetime.timedelta)) { - return py.False; - } - - return (this.days === other.days - && this.seconds === other.seconds - && this.microseconds === other.microseconds) - ? py.True : py.False; - }, - __add__: function (other) { - if (!py.PY_isInstance(other, datetime.timedelta)) { - return py.NotImplemented; - } - return py.PY_call(datetime.timedelta, [ - py.float.fromJSON(this.days + other.days), - py.float.fromJSON(this.seconds + other.seconds), - py.float.fromJSON(this.microseconds + other.microseconds) - ]); - }, - __radd__: function (other) { return this.__add__(other); }, - __sub__: function (other) { - if (!py.PY_isInstance(other, datetime.timedelta)) { - return py.NotImplemented; - } - return py.PY_call(datetime.timedelta, [ - py.float.fromJSON(this.days - other.days), - py.float.fromJSON(this.seconds - other.seconds), - py.float.fromJSON(this.microseconds - other.microseconds) - ]); - }, - __rsub__: function (other) { - if (!py.PY_isInstance(other, datetime.timedelta)) { - return py.NotImplemented; - } - return this.__neg__().__add__(other); - }, - __neg__: function () { - return py.PY_call(datetime.timedelta, [ - py.float.fromJSON(-this.days), - py.float.fromJSON(-this.seconds), - py.float.fromJSON(-this.microseconds) - ]); - }, - __pos__: function () { return this; }, - __mul__: function (other) { - if (!py.PY_isInstance(other, py.float)) { - return py.NotImplemented; - } - var n = other.toJSON(); - return py.PY_call(datetime.timedelta, [ - py.float.fromJSON(this.days * n), - py.float.fromJSON(this.seconds * n), - py.float.fromJSON(this.microseconds * n) - ]); - }, - __rmul__: function (other) { return this.__mul__(other); }, - __div__: function (other) { - if (!py.PY_isInstance(other, py.float)) { - return py.NotImplemented; - } - var usec = ((this.days * 24 * 3600) + this.seconds) * 1000000 - + this.microseconds; - return py.PY_call( - datetime.timedelta, [ - zero, zero, py.float.fromJSON(usec / other.toJSON())]); - }, - __floordiv__: function (other) { return this.__div__(other); }, - total_seconds: function () { - return py.float.fromJSON( - this.days * 86400 - + this.seconds - + this.microseconds / 1000000); - }, - __nonzero__: function () { - return (!!this.days || !!this.seconds || !!this.microseconds) - ? py.True - : py.False; - } -}); - -datetime.datetime = py.type('datetime', null, { - __init__: function () { - var zero = py.float.fromJSON(0); - var args = py.PY_parseArgs(arguments, [ - 'year', 'month', 'day', - ['hour', zero], ['minute', zero], ['second', zero], - ['microsecond', zero], ['tzinfo', py.None] - ]); - for(var key in args) { - if (!args.hasOwnProperty(key)) { continue; } - this[key] = asJS(args[key]); - } - }, - replace: function () { - var args = py.PY_parseArgs(arguments, [ - ['year', py.None], ['month', py.None], ['day', py.None], - ['hour', py.None], ['minute', py.None], ['second', py.None], - ['microsecond', py.None] // FIXME: tzinfo, can't use None as valid input - ]); - var params = {}; - for(var key in args) { - if (!args.hasOwnProperty(key)) { continue; } - - var arg = args[key]; - params[key] = (arg === py.None ? this[key] : asJS(arg)); - } - return py.PY_call(datetime.datetime, params); - }, - strftime: function () { - var self = this; - var args = py.PY_parseArgs(arguments, 'format'); - return py.str.fromJSON(args.format.toJSON() - .replace(/%([A-Za-z])/g, function (m, c) { - switch (c) { - case 'Y': return _.str.sprintf('%04d', self.year); - case 'm': return _.str.sprintf('%02d', self.month); - case 'd': return _.str.sprintf('%02d', self.day); - case 'H': return _.str.sprintf('%02d', self.hour); - case 'M': return _.str.sprintf('%02d', self.minute); - case 'S': return _.str.sprintf('%02d', self.second); - } - throw new Error('ValueError: No known conversion for ' + m); - })); - }, - now: py.classmethod.fromJSON(function () { - var d = new Date(); - return py.PY_call(datetime.datetime, [ - d.getFullYear(), d.getMonth() + 1, d.getDate(), - d.getHours(), d.getMinutes(), d.getSeconds(), - d.getMilliseconds() * 1000]); - }), - today: py.classmethod.fromJSON(function () { - var dt_class = py.PY_getAttr(datetime, 'datetime'); - return py.PY_call(py.PY_getAttr(dt_class, 'now')); - }), - utcnow: py.classmethod.fromJSON(function () { - var d = new Date(); - return py.PY_call(datetime.datetime, - [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), - d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), - d.getUTCMilliseconds() * 1000]); - }), - combine: py.classmethod.fromJSON(function () { - var args = py.PY_parseArgs(arguments, 'date time'); - return py.PY_call(datetime.datetime, [ - py.PY_getAttr(args.date, 'year'), - py.PY_getAttr(args.date, 'month'), - py.PY_getAttr(args.date, 'day'), - py.PY_getAttr(args.time, 'hour'), - py.PY_getAttr(args.time, 'minute'), - py.PY_getAttr(args.time, 'second') - ]); - }), - toJSON: function () { - return new Date( - this.year, - this.month - 1, - this.day, - this.hour, - this.minute, - this.second, - this.microsecond / 1000); - }, - __add__: function (other) { - if (!py.PY_isInstance(other, datetime.timedelta)) { - return py.NotImplemented; - } - var s = tmxxx(this.year, this.month, this.day + other.days, this.hour, this.minute, this.second + other.seconds); - return datetime.datetime.fromJSON(s.year, s.month, s.day, s.hour, s.minute, s.second); - }, - __sub__: function (other) { - if (py.PY_isInstance(other, datetime.timedelta)) { - return py.PY_add(this, py.PY_negative(other)); - } - return py.NotImplemented; - }, - fromJSON: function (year, month, day, hour, minute, second) { - return py.PY_call(datetime.datetime, [year, month, day, hour, minute, second]); - }, -}); - -datetime.date = py.type('date', null, { - __init__: function () { - var args = py.PY_parseArgs(arguments, 'year month day'); - this.year = asJS(args.year); - this.month = asJS(args.month); - this.day = asJS(args.day); - }, - strftime: function () { - var self = this; - var args = py.PY_parseArgs(arguments, 'format'); - return py.str.fromJSON(args.format.toJSON() - .replace(/%([A-Za-z])/g, function (m, c) { - switch (c) { - case 'Y': return self.year; - case 'm': return _.str.sprintf('%02d', self.month); - case 'd': return _.str.sprintf('%02d', self.day); - } - throw new Error('ValueError: No known conversion for ' + m); - })); - }, - __eq__: function (other) { - return (this.year === other.year - && this.month === other.month - && this.day === other.day) - ? py.True : py.False; - }, - replace: function () { - var args = py.PY_parseArgs(arguments, [ - ['year', py.None], ['month', py.None], ['day', py.None] - ]); - var params = {}; - for(var key in args) { - if (!args.hasOwnProperty(key)) { continue; } - - var arg = args[key]; - params[key] = (arg === py.None ? this[key] : asJS(arg)); - } - return py.PY_call(datetime.date, params); - }, - __add__: function (other) { - if (!py.PY_isInstance(other, datetime.timedelta)) { - return py.NotImplemented; - } - var s = tmxxx(this.year, this.month, this.day + other.days); - return datetime.date.fromJSON(s.year, s.month, s.day); - }, - __radd__: function (other) { return this.__add__(other); }, - __sub__: function (other) { - if (py.PY_isInstance(other, datetime.timedelta)) { - return py.PY_add(this, py.PY_negative(other)); - } - if (py.PY_isInstance(other, datetime.date)) { - // FIXME: getattr and sub API methods - return py.PY_call(datetime.timedelta, [ - py.PY_subtract( - py.PY_call(py.PY_getAttr(this, 'toordinal')), - py.PY_call(py.PY_getAttr(other, 'toordinal'))) - ]); - } - return py.NotImplemented; - }, - toordinal: function () { - return py.float.fromJSON(ymd2ord(this.year, this.month, this.day)); - }, - weekday: function () { - return py.float.fromJSON((this.toordinal().toJSON()+6)%7); - }, - fromJSON: function (year, month, day) { - return py.PY_call(datetime.date, [year, month, day]); - }, - today: py.classmethod.fromJSON(function () { - var d = new Date (); - return py.PY_call(datetime.date, [ - d.getFullYear(), d.getMonth() + 1, d.getDate()]); - }), -}); -/** - * Returns the current local date, which means the date on the client (which can be different - * compared to the date of the server). - * - * @return {datetime.date} - */ -function context_today() { - var d = new Date(); - return py.PY_call( - datetime.date, [d.getFullYear(), d.getMonth() + 1, d.getDate()]); -} - -datetime.time = py.type('time', null, { - __init__: function () { - var zero = py.float.fromJSON(0); - var args = py.PY_parseArgs(arguments, [ - ['hour', zero], ['minute', zero], ['second', zero], ['microsecond', zero], - ['tzinfo', py.None] - ]); - - for(var k in args) { - if (!args.hasOwnProperty(k)) { continue; } - this[k] = asJS(args[k]); - } - } -}); - -var time = py.PY_call(py.object); -time.strftime = py.PY_def.fromJSON(function () { - var args = py.PY_parseArgs(arguments, 'format'); - var dt_class = py.PY_getAttr(datetime, 'datetime'); - var d = py.PY_call(py.PY_getAttr(dt_class, 'utcnow')); - return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]); -}); - -var args = _.map(('year month day ' - + 'years months weeks days ' - + 'weekday leapdays yearday nlyearday').split(' '), function (arg) { - switch (arg) { - case 'years':case 'months':case 'days':case 'leapdays':case 'weeks': - return [arg, zero]; - case 'year':case 'month':case 'day':case 'weekday': - case 'yearday':case 'nlyearday': - return [arg, null]; - default: - throw new Error("Unknown relativedelta argument " + arg); - } -}); -args.unshift('*'); - -var _utils = { - monthrange: function (year, month) { - if (month < 1 || month > 12) { - throw new Error("Illegal month " + month); - } - - var day1 = this.weekday(year, month, 1); - var ndays = this.mdays[month] + (month == this.February && this.isleap(year)); - return [day1, ndays]; - }, - weekday: function (year, month, day) { - var date = py.PY_call(datetime.date, [year, month, day]); - return py.PY_call(py.PY_getAttr(date, 'weekday')); - }, - isleap: function (year) { - return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); - }, - mdays: [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], - January: 1, - February: 2 -}; - -var relativedelta = py.type('relativedelta', null, { - __init__: function () { - this.ops = py.PY_parseArgs(arguments, args); - this.ops.days = py.float.fromJSON( - asJS(this.ops.days) + asJS(this.ops.weeks) * 7 - ); - - var yday = zero; - if (this.ops.nlyearday) { - yday = this.ops.nlyearday; - } else if (this.ops.yearday) { - yday = this.ops.yearday; - if (asJS(this.ops.yearday) > 59) { - this.ops.leapdays = py.float.fromJS(-1); - } - } - if (py.PY_isTrue(yday)) { - var ydayidx = [31, 59, 90, 120, 151, 181, 212, - 243, 273, 304, 334, 366]; - for(var idx=0; idx<ydayidx.length; ++idx) { - var ydays = ydayidx[idx]; - if (asJS(yday) <= ydays) { - this.ops.month = py.float.fromJSON(idx+1); - if (!idx) { - this.ops.day = yday; - } else { - this.ops.day = py.PY_subtract( - yday, - py.float.fromJSON(ydayidx[idx-1]) - ); - } - break; - } - } - if (idx === ydayidx.length) { - throw new Error("Invalid year day (" + asJS(yday) + ")"); - } - } - this._fix(); - }, - _fix: function () { - var months = asJS(this.ops.months); - if (Math.abs(months) > 11) { - var s = months > 0 ? 1 : -1; - var r = utils.divmod(months * s, 12); - this.ops.months = py.float.fromJSON(r.mod*s); - this.ops.years = py.float.fromJSON( - asJS(this.ops.years) + r.div*s); - } - this._has_time = 0; - }, - __add__: function (other) { - if (!py.PY_isInstance(other, datetime.date)) { - return py.NotImplemented; - } - // TODO: test this whole mess - var year = (asJS(this.ops.year) || asJS(other.year)) + asJS(this.ops.years); - var month = asJS(this.ops.month) || asJS(other.month); - var months; - if (months = asJS(this.ops.months)) { - if (Math.abs(months) < 1 || Math.abs(months) > 12) { - throw new Error("Can only use relative months between -12 and +12"); - } - month += months; - if (month > 12) { - year += 1; - month -= 12; - } - if (month < 1) { - year -= 1; - month += 12; - } - } - - var day = Math.min(_utils.monthrange(year, month)[1], - asJS(this.ops.day) || asJS(other.day)); - - var repl = { - year: py.float.fromJSON(year), - month: py.float.fromJSON(month), - day: py.float.fromJSON(day) - }; - - var days = asJS(this.ops.days); - if (py.PY_isTrue(this.ops.leapdays) && month > 2 && _utils.isleap(year)) { - days += asJS(this.ops.leapdays); - } - - var ret = py.PY_add( - py.PY_call(py.PY_getAttr(other, 'replace'), repl), - py.PY_call(datetime.timedelta, { - days: py.float.fromJSON(days) - }) - ); - - if (this.ops.weekday) { - // FIXME: only handles numeric weekdays, not decorated - var weekday = asJS(this.ops.weekday), nth = 1; - var jumpdays = (Math.abs(nth) - 1) * 7; - - var ret_weekday = asJS(py.PY_call(py.PY_getAttr(ret, 'weekday'))); - if (nth > 0) { - jumpdays += (7-ret_weekday+weekday) % 7; - } else { - jumpdays += (ret_weekday - weekday) % 7; - jumpdays *= -1; - } - ret = py.PY_add( - ret, - py.PY_call(datetime.timedelta, { - days: py.float.fromJSON(jumpdays) - }) - ); - } - - return ret; - }, - __radd__: function (other) { - return this.__add__(other); - }, - __rsub__: function (other) { - return this.__neg__().__radd__(other); - }, - __neg__: function () { - return py.PY_call(relativedelta, { - years: py.PY_negative(this.ops.years), - months: py.PY_negative(this.ops.months), - days: py.PY_negative(this.ops.days), - leapdays: this.ops.leapdays, - year: this.ops.year, - month: this.ops.month, - day: this.ops.day, - weekday: this.ops.weekday - }); - } -}); - -// recursively wraps JS objects passed into the context to attributedicts -// which jsonify back to JS objects -function wrap(value) { - if (value === null) { return py.None; } - - switch (typeof value) { - case 'undefined': throw new Error("No conversion for undefined"); - case 'boolean': return py.bool.fromJSON(value); - case 'number': return py.float.fromJSON(value); - case 'string': return py.str.fromJSON(value); - } - - switch(value.constructor) { - case Object: return wrapping_dict.fromJSON(value); - case Array: return wrapping_list.fromJSON(value); - } - - throw new Error("ValueError: unable to wrap " + value); -} - -var wrapping_dict = py.type('wrapping_dict', null, { - __init__: function () { - this._store = {}; - }, - __getitem__: function (key) { - var k = key.toJSON(); - if (!(k in this._store)) { - throw new Error("KeyError: '" + k + "'"); - } - return wrap(this._store[k]); - }, - __getattr__: function (key) { - return this.__getitem__(py.str.fromJSON(key)); - }, - __len__: function () { - return Object.keys(this._store).length; - }, - __nonzero__: function () { - return py.PY_size(this) > 0 ? py.True : py.False; - }, - get: function () { - var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]); - - if (!(args.k.toJSON() in this._store)) { return args.d; } - return this.__getitem__(args.k); - }, - fromJSON: function (d) { - var instance = py.PY_call(wrapping_dict); - instance._store = d; - return instance; - }, - toJSON: function () { - return this._store; - }, -}); - -var wrapping_list = py.type('wrapping_list', null, { - __init__: function () { - this._store = []; - }, - __getitem__: function (index) { - return wrap(this._store[index.toJSON()]); - }, - __len__: function () { - return this._store.length; - }, - __nonzero__: function () { - return py.PY_size(this) > 0 ? py.True : py.False; - }, - fromJSON: function (ar) { - var instance = py.PY_call(wrapping_list); - instance._store = ar; - return instance; - }, - toJSON: function () { - return this._store; - }, -}); - -function wrap_context(context) { - for (var k in context) { - if (!context.hasOwnProperty(k)) { continue; } - var val = context[k]; - - if (val === null) { continue; } - if (val.constructor === Array) { - context[k] = wrapping_list.fromJSON(val); - } else if (val.constructor === Object - && !py.PY_isInstance(val, py.object)) { - context[k] = wrapping_dict.fromJSON(val); - } - } - return context; -} - -function eval_contexts(contexts, evaluation_context) { - evaluation_context = _.extend(pycontext(), evaluation_context || {}); - return _(contexts).reduce(function (result_context, ctx) { - // __eval_context evaluations can lead to some of `contexts`'s - // values being null, skip them as well as empty contexts - if (_.isEmpty(ctx)) { return result_context; } - if (_.isString(ctx)) { - // wrap raw strings in context - ctx = { __ref: 'context', __debug: ctx }; - } - var evaluated = ctx; - switch(ctx.__ref) { - case 'context': - evaluation_context.context = evaluation_context; - evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context)); - break; - case 'compound_context': - var eval_context = eval_contexts([ctx.__eval_context]); - evaluated = eval_contexts( - ctx.__contexts, _.extend({}, evaluation_context, eval_context)); - break; - } - // add newly evaluated context to evaluation context for following - // siblings - _.extend(evaluation_context, evaluated); - return _.extend(result_context, evaluated); - }, {}); -} - -function eval_domains(domains, evaluation_context) { - evaluation_context = _.extend(pycontext(), evaluation_context || {}); - var result_domain = []; - // Normalize only if the first domain is the array ["|"] or ["!"] - var need_normalization = ( - domains && - domains.length > 0 && - domains[0].length === 1 && - (domains[0][0] === "|" || domains[0][0] === "!") - ); - _(domains).each(function (domain) { - if (_.isString(domain)) { - // wrap raw strings in domain - domain = { __ref: 'domain', __debug: domain }; - } - var domain_array_to_combine; - switch(domain.__ref) { - case 'domain': - evaluation_context.context = evaluation_context; - domain_array_to_combine = py.eval(domain.__debug, wrap_context(evaluation_context)); - break; - default: - domain_array_to_combine = domain; - } - if (need_normalization) { - domain_array_to_combine = get_normalized_domain(domain_array_to_combine); - } - result_domain.push.apply(result_domain, domain_array_to_combine); - }); - return result_domain; -} - -/** - * Returns a normalized copy of the given domain array. Normalization is - * is making the implicit "&" at the start of the domain explicit, e.g. - * [A, B, C] would become ["&", "&", A, B, C]. - * - * @param {Array} domain_array - * @returns {Array} normalized copy of the given array - */ -function get_normalized_domain(domain_array) { - var expected = 1; // Holds the number of expected domain expressions - _.each(domain_array, function (item) { - if (item === "&" || item === "|") { - expected++; - } else if (item !== "!") { - expected--; - } - }); - var new_explicit_ands = _.times(-expected, _.constant("&")); - return new_explicit_ands.concat(domain_array); -} - -function eval_groupbys(contexts, evaluation_context) { - evaluation_context = _.extend(pycontext(), evaluation_context || {}); - var result_group = []; - _(contexts).each(function (ctx) { - if (_.isString(ctx)) { - // wrap raw strings in context - ctx = { __ref: 'context', __debug: ctx }; - } - var group; - var evaluated = ctx; - switch(ctx.__ref) { - case 'context': - evaluation_context.context = evaluation_context; - evaluated = py.eval(ctx.__debug, wrap_context(evaluation_context)); - break; - case 'compound_context': - var eval_context = eval_contexts([ctx.__eval_context]); - evaluated = eval_contexts( - ctx.__contexts, _.extend({}, evaluation_context, eval_context)); - break; - } - group = evaluated.group_by; - if (!group) { return; } - if (typeof group === 'string') { - result_group.push(group); - } else if (group instanceof Array) { - result_group.push.apply(result_group, group); - } else { - throw new Error('Got invalid groupby {{' - + JSON.stringify(group) + '}}'); - } - _.extend(evaluation_context, evaluated); - }); - return result_group; -} - - -function pycontext() { - return { - datetime: datetime, - context_today: context_today, - time: time, - relativedelta: relativedelta, - current_date: py.PY_call( - time.strftime, [py.str.fromJSON('%Y-%m-%d')]), - }; -} - -/** - * @param {String} type "domains", "contexts" or "groupbys" - * @param {Array} object domains or contexts to evaluate - * @param {Object} [context] evaluation context - */ -function pyeval(type, object, context) { - context = _.extend(pycontext(), context || {}); - - //noinspection FallthroughInSwitchStatementJS - switch(type) { - case 'context': - case 'contexts': - if (type === 'context') { - object = [object]; - } - return eval_contexts(object, context); - case 'domain': - case 'domains': - if (type === 'domain') - object = [object]; - return eval_domains(object, context); - case 'groupbys': - return eval_groupbys(object, context); - } - throw new Error("Unknow evaluation type " + type); -} - -function eval_arg(arg) { - if (typeof arg !== 'object' || !arg.__ref) { return arg; } - switch(arg.__ref) { - case 'domain': - return pyeval('domains', [arg]); - case 'context': case 'compound_context': - return pyeval('contexts', [arg]); - default: - throw new Error(_t("Unknown nonliteral type ") + ' ' + arg.__ref); - } -} - -/** - * If args or kwargs are unevaluated contexts or domains (compound or not), - * evaluated them in-place. - * - * Potentially mutates both parameters. - * - * @param args - * @param kwargs - */ -function ensure_evaluated(args, kwargs) { - for (var i=0; i<args.length; ++i) { - args[i] = eval_arg(args[i]); - } - for (var k in kwargs) { - if (!kwargs.hasOwnProperty(k)) { continue; } - kwargs[k] = eval_arg(kwargs[k]); - } -} - -function eval_domains_and_contexts(source) { - // see Session.eval_context in Python - return { - context: pyeval('contexts', source.contexts || [], source.eval_context), - domain: pyeval('domains', source.domains, source.eval_context), - group_by: pyeval('groupbys', source.group_by_seq || [], source.eval_context), - }; -} - -function py_eval(expr, context) { - return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null})); -} - - -return { - context: pycontext, - ensure_evaluated: ensure_evaluated, - eval: pyeval, - eval_domains_and_contexts: eval_domains_and_contexts, - py_eval: py_eval, -}; - -}); diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index ffeb4a1de87..3068a6528c9 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -52,20 +52,6 @@ var utils = { return Math.max(min, Math.min(max, val)); }, /** - * computes (Math.floor(a/b), a%b and passes that to the callback. - * - * returns the callback's result - */ - divmod: function (a, b, fn) { - var mod = a%b; - // in python, sign(a % b) === sign(b). Not in JS. If wrong side, add a - // round of b - if (mod > 0 && b < 0 || mod < 0 && b > 0) { - mod += b; - } - return fn(Math.floor(a/b), mod); - }, - /** * @param {number} value * @param {integer} decimals * @returns {boolean} @@ -290,17 +276,6 @@ var utils = { return new Array(size - str.length + 1).join('0') + str; }, /** - * Passes the fractional and integer parts of x to the callback, returns - * the callback's result - */ - modf: function (x, fn) { - var mod = x%1; - if (mod < 0) { - mod += 1; - } - return fn(mod, Math.floor(x)); - }, - /** * performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision * See the corresponding float_round() in server/tools/float_utils.py for more info * @param {Number} value the value to be rounded diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js index 2c4ed711223..7bc5e95bfc8 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -15,7 +15,7 @@ var BasicController = require('web.BasicController'); var BasicModel = require('web.BasicModel'); var config = require('web.config'); var fieldRegistry = require('web.field_registry'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var utils = require('web.utils'); var BasicView = AbstractView.extend({ @@ -216,13 +216,13 @@ var BasicView = AbstractView.extend({ attrs.decorations = attrs.decorations || []; attrs.decorations.push({ className: 'text-' + splitKey[1], - expression: py.parse(py.tokenize(value)) + expression: pyUtils._getPyJSAST(value), }); } }); if (!_.isObject(attrs.options)) { // parent arch could have already been processed (TODO this should not happen) - attrs.options = attrs.options ? pyeval.py_eval(attrs.options) : {}; + attrs.options = attrs.options ? pyUtils.py_eval(attrs.options) : {}; } if (attrs.on_change && !field.onChange) { diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index ae1a8f7305b..a210c07a885 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -11,7 +11,7 @@ var core = require('web.core'); var BasicController = require('web.BasicController'); var DataExport = require('web.DataExport'); var Dialog = require('web.Dialog'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var Sidebar = require('web.Sidebar'); var _t = core._t; @@ -67,7 +67,7 @@ var ListController = BasicController.extend({ if (this.$('thead .o_list_record_selector input').prop('checked')) { var searchData = this.searchView.build_search_data(); var userContext = this.getSession().user_context; - var results = pyeval.eval_domains_and_contexts({ + var results = pyUtils.eval_domains_and_contexts({ domains: searchData.domains, contexts: [userContext].concat(searchData.contexts), group_by_seq: searchData.groupbys || [] diff --git a/addons/web/static/src/js/views/search/favorites_menu.js b/addons/web/static/src/js/views/search/favorites_menu.js index f0962e6177e..1e62ffb5ce3 100644 --- a/addons/web/static/src/js/views/search/favorites_menu.js +++ b/addons/web/static/src/js/views/search/favorites_menu.js @@ -4,7 +4,8 @@ odoo.define('web.FavoriteMenu', function (require) { var config = require('web.config'); var core = require('web.core'); var data_manager = require('web.data_manager'); -var pyeval = require('web.pyeval'); +var Domain = require('web.Domain'); +var pyUtils = require('web.py_utils'); var session = require('web.session'); var Widget = require('web.Widget'); @@ -98,15 +99,17 @@ return Widget.extend({ return; } var user_context = this.getSession().user_context; - var search = this.searchview.build_search_data(); + // get search params with domains not evaluated + var search = this.searchview.build_search_data(true); + var domain = pyUtils.assembleDomains(search.domains, 'AND'); var controllerContext; this.trigger_up('get_controller_context', { callback: function (ctx) { controllerContext = ctx; }, }); - var results = pyeval.eval_domains_and_contexts({ - domains: search.domains, + var results = pyUtils.eval_domains_and_contexts({ + domains: [], contexts: [user_context].concat(search.contexts.concat(controllerContext || [])), group_by_seq: search.groupbys || [], }); @@ -125,7 +128,7 @@ return Widget.extend({ user_id: shared_filter ? false : session.uid, model_id: this.target_model, context: results.context, - domain: results.domain, + domain: domain, sort: JSON.stringify(this.searchview.dataset._sort), is_default: default_filter, action_id: this.action_id, @@ -180,13 +183,23 @@ return Widget.extend({ * @return {Object} */ facet_for: function (filter) { + var self = this; return { category: _t("Custom Filter"), icon: 'fa-star', field: { get_context: function () { return filter.context; }, get_groupby: function () { return [filter.context]; }, - get_domain: function () { return filter.domain; } + // facet is not used + get_domain: function (facet, noEvaluation) { + noEvaluation = noEvaluation || false; + var userContext = self.getSession().user_context; + var domain = pyUtils.assembleDomains([filter.domain]); + if (!noEvaluation) { + return Domain.prototype.stringToArray(domain, userContext); + } + return domain; + } }, _id: filter.id, is_custom_filter: true, diff --git a/addons/web/static/src/js/views/search/filters_menu.js b/addons/web/static/src/js/views/search/filters_menu.js index 778e199df44..221f98faa97 100644 --- a/addons/web/static/src/js/views/search/filters_menu.js +++ b/addons/web/static/src/js/views/search/filters_menu.js @@ -3,14 +3,15 @@ odoo.define('web.FiltersMenu', function (require) { var config = require('web.config'); var core = require('web.core'); +var Domain = require('web.Domain'); var DropdownMenu = require('web.DropdownMenu'); var search_filters = require('web.search_filters'); +var time = require('web.time'); var QWeb = core.qweb; var _t = core._t; - var FiltersMenu = DropdownMenu.extend({ custom_events: { remove_proposition: '_onRemoveProposition', @@ -20,6 +21,7 @@ var FiltersMenu = DropdownMenu.extend({ 'click .o_add_custom_filter': '_onAddCustomFilterClick', 'click .o_add_condition': '_appendProposition', 'click .o_apply_filter': '_onApplyClick', + ':hover .o_item_option': '_onOptionHover', }), /** @@ -29,6 +31,8 @@ var FiltersMenu = DropdownMenu.extend({ * interface IFilter { * itemId: string; unique id associated with the filter * domain: string; + * isPeriod: boolean + * fieldName: string; fieldName used to generate a domain in case isDate is true (and domain empty) * description: string; label printed on screen * groupId: string; * isActive: boolean; (optional) determines if the filter is considered active @@ -46,16 +50,21 @@ var FiltersMenu = DropdownMenu.extend({ // determines where the filter menu is displayed and its style this.isMobile = config.device.isMobile; // determines list of options used by filter of type 'date' - // TO DO: simplify optionIds below - this.intervalOptions = [ - {description: _t('Day'), optionId: '__interval__day__'}, - {description: _t('Week'), optionId: '__interval__week__'}, - {description: _t('Month'), optionId: '__interval__month__'}, - {description: _t('Year'), optionId: '__interval__year__'}, + this.periodOptions = [ + {description: _t('Today'), optionId: 'today', groupId: 1}, + {description: _t('This Week'), optionId: 'this_week', groupId: 1}, + {description: _t('This Month'), optionId: 'this_month', groupId: 1}, + {description: _t('This Quarter'), optionId: 'this_quarter', groupId: 1}, + {description: _t('This Year'), optionId: 'this_year', groupId: 1}, + {description: _t('Yesterday'), optionId: 'yesterday', groupId: 2}, + {description: _t('Last Week'), optionId: 'last_week', groupId: 2}, + {description: _t('Last Month'), optionId: 'last_month', groupId: 2}, + {description: _t('Last Quarter'), optionId: 'last_quarter', groupId: 2}, + {description: _t('Last Year'), optionId: 'last_year', groupId: 2}, + {description: _t('Last 7 Days'), optionId: 'last_7_days', groupId: 3}, + {description: _t('Last 30 Days'), optionId: 'last_30_days', groupId: 3}, + {description: _t('Last 365 Days'), optionId: 'last_365_days', groupId: 3}, ]; - // determines the default option used in case - // it has not been provided for a filter - this.defaultOptionId = "__interval__month__"; // determines when the 'Add custom filter' submenu is open this.generatorMenuIsOpen = false; this.propositions = []; @@ -111,8 +120,11 @@ var FiltersMenu = DropdownMenu.extend({ _commitSearch: function () { var self = this; var filters = _.invoke(this.propositions, 'get_filter'); + _.each(filters, function (filter) { + filter.attrs.domain = Domain.prototype.arrayToString(filter.attrs.domain); + }); var groupId = _.uniqueId('__group__'); - var toSendToSearchView = []; + var data = []; filters.forEach(function (filter) { var filterName = _.uniqueId('__filter__'); var filterItem = { @@ -122,7 +134,7 @@ var FiltersMenu = DropdownMenu.extend({ isActive: true, }; self._prepareItem(filterItem); - toSendToSearchView.push({ + data.push({ itemId: filterName, groupId: groupId, filter: filter, @@ -130,13 +142,59 @@ var FiltersMenu = DropdownMenu.extend({ self.items.push(filterItem); }); this._renderMenuItems(); - this.trigger_up('new_filter', toSendToSearchView); + this.trigger_up('new_filters', data); _.invoke(this.propositions, 'destroy'); this.propositions = []; - // this._appendProposition(); this._toggleCustomFilterMenu(); }, /** + * override + * + * @private + * @param {Object} item + */ + _prepareItem: function (item) { + if (item.isPeriod) { + item.options = this.periodOptions; + } + // super has to be called here because we need to add options to groupby + // before to call it since some keys in a groupby are initialized using + // the keys 'options' and 'defaultOptionId' + this._super.apply(this, arguments); + }, + /** + * override + * + * @private + */ + _renderMenuItems: function () { + var self= this; + this._super.apply(this, arguments); + // the following code adds tooltip on date options in order + // to alert the user of the meaning of intervals + var $options = this.$('ul.o_dropdown_menu .o_item_option'); + $options.each(function () { + var $option = $(this); + $option.tooltip({ + delay: { show: 500, hide: 0 }, + title: function () { + var itemId = $option.attr('data-item_id'); + var optionId = $option.attr('data-option_id'); + var fieldName = _.findWhere(self.items, {itemId: itemId}).fieldName; + var domain = Domain.prototype.constructDomain(fieldName, optionId, 'date', true); + var evaluatedDomain = Domain.prototype.stringToArray(domain); + var dateFormat = time.getLangDateFormat(); + var dateStart = moment(evaluatedDomain[1][2], "YYYY-MM-DD", 'en').format(dateFormat); + var dateEnd = moment(evaluatedDomain[2][2], "YYYY-MM-DD", 'en').format(dateFormat); + if (optionId === 'today' || optionId === 'yesterday') { + return dateStart; + } + return _.str.sprintf(_t('From %s To %s'), dateStart, dateEnd); + } + }); + }); + }, + /** * Hide and display the sub menu which allows adding custom filters * * @private diff --git a/addons/web/static/src/js/views/search/groupby_menu.js b/addons/web/static/src/js/views/search/groupby_menu.js index 4e3c4eb573d..f32cf314a33 100644 --- a/addons/web/static/src/js/views/search/groupby_menu.js +++ b/addons/web/static/src/js/views/search/groupby_menu.js @@ -9,6 +9,8 @@ var QWeb = core.qweb; var GROUPABLE_TYPES = ['many2one', 'char', 'boolean', 'selection', 'date', 'datetime']; +var DEFAULT_INTERVAL = 'month'; + var GroupByMenu = DropdownMenu.extend({ events: _.extend({}, DropdownMenu.prototype.events, { @@ -26,12 +28,12 @@ var GroupByMenu = DropdownMenu.extend({ * fieldName: string; field name without interval! * description: string; label printed on screen * groupId: string; + * isDate: boolean; * isActive: boolean; (optional) determines if the groupby is considered active * isOpen: boolean; (optional) in case there are options the submenu presenting the options * is opened or closed according to isOpen * isRemovable: boolean; (optional) can be removed from menu * options: array of objects with 'optionId' and 'description' keys; (optional) - * defaultOptionId: string refers to an optionId (optional) * currentOptionId: string refers to an optionId that is activated if item is active (optional) * } * @param {Object} fields 'field_get' of a model: mapping from field name @@ -46,15 +48,12 @@ var GroupByMenu = DropdownMenu.extend({ this.generatorMenuIsOpen = false; // determines list of options used by groupbys of type 'date' this.intervalOptions = [ - {description: 'Day', optionId: 'day'}, - {description: 'Week', optionId: 'week'}, - {description: 'Month', optionId: 'month'}, - {description: 'Quarter', optionId: 'quarter'}, - {description: 'Year', optionId: 'year'}, + {description: 'Day', optionId: 'day', groupId: 1}, + {description: 'Week', optionId: 'week', groupId: 1}, + {description: 'Month', optionId: 'month', groupId: 1}, + {description: 'Quarter', optionId: 'quarter', groupId: 1}, + {description: 'Year', optionId: 'year', groupId: 1}, ]; - // determines the default option used in case - // it has not been provided for a groupby - this.defaultOptionId = "month"; this.groupableFields = []; _.each(fields, function (field, name) { if (field.sortable && _.contains(GROUPABLE_TYPES, field.type)) { @@ -114,7 +113,7 @@ var GroupByMenu = DropdownMenu.extend({ * method called via the 'Add Custom Groupby' menu to create * a new groupby and add it (activated) to the menu * In case the field is of type date, it is activated using - * the option determined by the parameter 'this.defaultOptionId' + * the option determined by the parameter 'DEFAULT_INTERVAL' * * @private * @param {string} fieldName @@ -132,8 +131,10 @@ var GroupByMenu = DropdownMenu.extend({ var eventData = _.clone(groupby); this._prepareItem(groupby); if (groupby.hasOptions) { - groupby.currentOptionId = groupby.defaultOptionId; + groupby.currentOptionId = DEFAULT_INTERVAL; + groupby.isDate = true; eventData.optionId = groupby.currentOptionId; + eventData.isDate = true; } this.items.push(groupby); var fieldIndex = this.presentedFields.indexOf(field); @@ -151,7 +152,6 @@ var GroupByMenu = DropdownMenu.extend({ _prepareItem: function (item) { if (_.contains(['date', 'datetime'], this.fields[item.fieldName].type)) { item.options = this.intervalOptions; - item.defaultOptionId = item.defaultOptionId || this.defaultOptionId; } // the idea is that we have only one group in the groupby menu // like in the search view. diff --git a/addons/web/static/src/js/views/search/search_inputs.js b/addons/web/static/src/js/views/search/search_inputs.js index 3366fe91ecb..ee179d7da74 100644 --- a/addons/web/static/src/js/views/search/search_inputs.js +++ b/addons/web/static/src/js/views/search/search_inputs.js @@ -5,7 +5,7 @@ var Context = require('web.Context'); var core = require('web.core'); var Domain = require('web.Domain'); var field_utils = require('web.field_utils'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var time = require('web.time'); var utils = require('web.utils'); var Widget = require('web.Widget'); @@ -416,7 +416,7 @@ var ManyToOneField = CharField.extend({ expand: function (needle) { var self = this; // FIXME: "concurrent" searches (multiple requests, mis-ordered responses) - var context = pyeval.eval( + var context = pyUtils.eval( 'contexts', [this.searchview.dataset.get_context()]); var args = this.attrs.domain; if (typeof args === 'string') { @@ -458,7 +458,7 @@ var ManyToOneField = CharField.extend({ // to handle this as if it were a single value. value = value[0]; } - var context = pyeval.eval('contexts', [this.searchview.dataset.get_context()]); + var context = pyUtils.eval('contexts', [this.searchview.dataset.get_context()]); return this._rpc({ model: this.attrs.relation, @@ -514,14 +514,17 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ * * @param {Array<instance.web.search.Filter>} filters elements of the group * @param {instance.web.SearchView} parent parent in which the filters are contained + * @param {Object} intervalMapping, a key is a field name and the corresponding value + * is the current interval used + * (necessarily the field is of type 'date' or 'datetime') */ - init: function (filters, parent, intervalMapping) { + init: function (filters, parent, intervalMapping, periodMapping) { // If all filters are group_by and we're not initializing a GroupbyGroup, // create a GroupbyGroup instead of the current FilterGroup if (!(this instanceof GroupbyGroup) && _(filters).all(function (f) { if (!f.attrs.context) { return false; } - var c = pyeval.eval('context', f.attrs.context); + var c = pyUtils.eval('context', f.attrs.context); return !_.isEmpty(c.group_by);})) { return new GroupbyGroup(filters, parent, intervalMapping); } @@ -529,6 +532,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ this.filters = filters; this.searchview = parent; this.intervalMapping = intervalMapping; + this.periodMapping = periodMapping || []; this.searchview.query.on('add remove change reset', this.proxy('search_change')); }, start: function () { @@ -637,29 +641,45 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ * Handles domains-fetching for all the filters within it: groups them. * * @param {VS.model.SearchFacet} facet + * @param {boolean} noEvaluation determines if the domains are evaluated or not. + By default, domains are evaluated. * @return {*} combined domains of the enabled filters in this group */ - get_domain: function (facet) { + get_domain: function (facet, noEvaluation) { + noEvaluation = noEvaluation || false; + var self = this; var userContext = this.getSession().user_context; var domains = facet.values.chain() - .map(function (f) { return f.get('value').attrs.domain; }) + .map(function (f) { + var filter = f.get('value'); + var attributes = filter.attrs; + var domain = attributes.domain; + var fieldName = attributes.date; + if (!domain && fieldName) { + var couple = _.findWhere(self.periodMapping, {filter: filter}); + var period = couple ? couple.period: 'this_month'; + var type = couple ? couple.type: 'date'; + domain = Domain.prototype.constructDomain(fieldName, period, type); + } + return domain; + }) .without('[]') .reject(_.isEmpty) - .map(function (d) { - return Domain.prototype.stringToArray(d, userContext); + .map(function (domain) { + domain = domain.replace(/%%/g, '%'); + return domain; }) .value(); if (!domains.length) { return; } - if (domains.length === 1) { - return domains[0]; + // domains belong the same groups. We need to make a union. + var domain = pyUtils.assembleDomains(domains, 'OR'); + if (!noEvaluation) { + return Domain.prototype.stringToArray(domain, userContext); } - - _.each(domains, Domain.prototype.normalizeArray); - var ors = _.times(domains.length - 1, _.constant("|")); - return ors.concat.apply(ors, domains); + return domain; }, toggle_filter: function (e) { e.preventDefault(); @@ -701,9 +721,22 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ }; })); }, + /* + * @param {Object} intervalMapping + */ updateIntervalMapping: function (intervalMapping) { this.intervalMapping = intervalMapping; - } + }, + /* + * @param {Object} periodMapping + */ + updatePeriodMapping: function (periodMapping) { + this.intervalMapping = periodMapping; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- }); diff --git a/addons/web/static/src/js/views/search/search_view.js b/addons/web/static/src/js/views/search/search_view.js index e307ca2a240..91075bc4972 100644 --- a/addons/web/static/src/js/views/search/search_view.js +++ b/addons/web/static/src/js/views/search/search_view.js @@ -4,10 +4,11 @@ odoo.define('web.SearchView', function (require) { var AutoComplete = require('web.AutoComplete'); var config = require('web.config'); var core = require('web.core'); +var Domain = require('web.Domain'); var FavoriteMenu = require('web.FavoriteMenu'); var FiltersMenu = require('web.FiltersMenu'); var GroupByMenu = require('web.GroupByMenu'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var search_inputs = require('web.search_inputs'); var utils = require('web.utils'); var Widget = require('web.Widget'); @@ -21,6 +22,9 @@ var FacetValues = Backbone.Collection.extend({ model: FacetValue }); +var DEFAULT_INTERVAL = 'month'; +var DEFAULT_PERIOD = 'this_month'; + var Facet = Backbone.Model.extend({ initialize: function (attrs) { var values = attrs.values; @@ -187,10 +191,21 @@ var FacetView = Widget.extend({ } } }, - init: function (parent, model, intervalMapping) { + /* + * @param {Widget} parent + * @param {Object} model + * @param {Object} intervalMapping, a key is a field name and the corresponding value + * is the current interval used + * (necessarily the field is of type 'date' or 'datetime') + * @param {Object} periodMapping, a key is a field name and the corresponding value + * is the current interval used + * (necessarily the field is of type 'date' or 'datetime') + */ + init: function (parent, model, intervalMapping, periodMapping) { this._super(parent); this.model = model; this.intervalMapping = intervalMapping; + this.periodMapping = periodMapping; this.model.on('change', this.model_changed, this); }, destroy: function () { @@ -205,9 +220,21 @@ var FacetView = Widget.extend({ if (index > 0) { $('<span/>', {html: self.model.get('separator') || _t(" or ")}).addClass('o_facet_values_sep').appendTo($e); } - var couple = _.findWhere(self.intervalMapping, {groupby: value.attributes.value}); - var interval = couple ? couple.interval : undefined; - return new FacetValueView(self, value, interval).appendTo($e); + var couple; + var option; + if (value.attributes.value && value.attributes.value.attrs) { + if (value.attributes.value.attrs.isPeriod) { + couple = _.findWhere(self.periodMapping, {filter: value.attributes.value}); + option = couple ? couple.period : value.attributes.value.attrs.default_period; + } + if (value.attributes.value.attrs.isDate) { + couple = _.findWhere(self.intervalMapping, {groupby: value.attributes.value}); + option = couple ? + couple.interval : + (value.attributes.value.attrs.defaultInterval || DEFAULT_INTERVAL); + } + } + return new FacetValueView(self, value, option).appendTo($e); })); }); }, @@ -218,21 +245,41 @@ var FacetView = Widget.extend({ var FacetValueView = Widget.extend({ template: 'SearchView.FacetView.Value', - init: function (parent, model, interval) { + /* + * @param {Widget} parent + * @param {Object} model + * @param {Object} interval (optional) is used in case the facet value + * corresponds to a groupby with an associated 'date' + * 'datetime' field. + */ + init: function (parent, model, option) { this._super(parent); this.model = model; - var intervalDescription = { + var optionDescription = _.extend({}, { day: 'Day', week: 'Week', month: 'Month', quarter: 'Quarter', year: 'Year', - }; - // to do: put a test on interval - if (interval) { - var intervalLabel = intervalDescription[interval]; - this.intervalLabel = _t(intervalLabel); + }, { + today: 'Today', + this_week: 'This Week', + this_month: 'This Month', + this_quarter: 'This Quarter', + this_year: 'This Year', + yesterday: 'Yesterday', + last_week: 'Last Week', + last_month: 'Last Month', + last_quarter: 'Last Quarter', + last_year: 'Last Year', + last_7_days: 'Last 7 Days', + last_30_days: 'Last 30 Days', + last_365_days: 'Last 365 Days', + }); + if (option) { + var optionLabel = optionDescription[option]; + this.optionLabel = _t(optionLabel); } this.model.on('change', this.model_changed, this); }, @@ -275,10 +322,10 @@ var SearchView = Widget.extend({ }, }, custom_events: { - 'menu_item_toggled': '_onItemToggled', - 'item_option_changed': '_onItemOptionChanged', - 'new_groupby': '_onNewGroupby', - 'new_filter': '_onNewFilter', + menu_item_toggled: '_onItemToggled', + item_option_changed: '_onItemOptionChanged', + new_groupby: '_onNewGroupby', + new_filters: '_onNewFilters', }, defaults: _.extend({}, Widget.prototype.defaults, { hidden: false, @@ -312,6 +359,8 @@ var SearchView = Widget.extend({ this.action = this.options.action || {}; this.search_fields = []; + this.hasFavorites = false; + this.selectedGroupIds = { groupByCategory: [], filterCategory: [] @@ -320,6 +369,7 @@ var SearchView = Widget.extend({ this.groupbysMapping = []; this.filtersMapping = []; this.intervalMapping = []; + this.periodMapping = []; this.filters = []; this.groupbys = []; @@ -392,13 +442,15 @@ var SearchView = Widget.extend({ var self = this, default_custom_filter = this.$buttons && this.favorite_menu && this.favorite_menu.get_default_filter(); if (!self.options.disable_custom_filters && default_custom_filter) { + this.hasFavorites = true; return this.favorite_menu.toggle_filter(default_custom_filter, true); } if (!_.isEmpty(this.search_defaults)) { - var inputs = this.search_fields.concat(this.filters, this.groupbys), - search_defaults = _.invoke(inputs, 'facet_for_defaults', this.search_defaults); + var inputs = this.search_fields.concat(this.filters, this.groupbys); + var search_defaults = _.invoke(inputs, 'facet_for_defaults', this.search_defaults); return $.when.apply(null, search_defaults).then(function () { - self.query.reset(_(arguments).compact(), {preventSearch: true}); + var facets = _.compact(arguments); + self.query.reset(facets, {preventSearch: true}); }); } this.query.reset([], {preventSearch: true}); @@ -424,6 +476,9 @@ var SearchView = Widget.extend({ this.trigger_up('search', search); }, /** + * @param {boolean} noDomainEvaluation determines if domain are evaluated or not. + * By default, domains are evaluated. + * * Extract search data from the view's facets. * * Result is an object with 3 (own) properties: @@ -437,12 +492,13 @@ var SearchView = Widget.extend({ * * @return {Object} */ - build_search_data: function () { + build_search_data: function (noDomainEvaluation) { var domains = [], contexts = [], groupbys = []; - + noDomainEvaluation = noDomainEvaluation || false; this.query.each(function (facet) { + // field is actually a FilterGroup! var field = facet.get('field'); - var domain = field.get_domain(facet); + var domain = field.get_domain(facet, noDomainEvaluation); if (domain) { domains.push(domain); } @@ -456,7 +512,6 @@ var SearchView = Widget.extend({ } }); var intervalMappingNormalized = {}; - _.each(this.intervalMapping, function (couple) { var fieldName = couple.groupby.fieldName; var interval = couple.interval; @@ -575,7 +630,6 @@ var SearchView = Widget.extend({ groupByCategory: [], filterCategory: [], }; - this.query.each(function (facet) { if (facet.attributes.cat === "groupByCategory") { @@ -609,7 +663,7 @@ var SearchView = Widget.extend({ ); } - var f = new FacetView(this, facet, this.intervalMapping); + var f = new FacetView(this, facet, this.intervalMapping, this.periodMapping); started.push(f.appendTo(self.$('.o_searchview_input_container'))); self.input_subviews.push(f); }, this); @@ -669,14 +723,20 @@ var SearchView = Widget.extend({ var category = 'filters'; if (item.attrs.context) { try { - var context = pyeval.eval('context', item.attrs.context); + var context = pyUtils.eval('context', item.attrs.context); if (context.group_by) { category = 'group_by'; item.attrs.fieldName = context.group_by.split(':')[0]; + item.attrs.isDate = _.contains(['date', 'datetime'], self.fields[item.attrs.fieldName].type); item.attrs.defaultInterval = context.group_by.split(':')[1]; } } catch (e) {} } + if (item.attrs.date) { + item.attrs.default_period = item.attrs.default_period || DEFAULT_PERIOD; + item.attrs.type = self.fields[item.attrs.date].type; + } + item.attrs.isPeriod = !!item.attrs.date; return { item: item, category: category, @@ -691,7 +751,7 @@ var SearchView = Widget.extend({ return current_group.push(new search_inputs.Filter(filter.item, self)); } if (current_group.length) { - var group = new search_inputs.FilterGroup(current_group, self, self.intervalMapping); + var group = new search_inputs.FilterGroup(current_group, self, self.intervalMapping, self.periodMapping); categories[current_category].push(group); current_group = []; } @@ -704,7 +764,6 @@ var SearchView = Widget.extend({ if (field.type === "many2one" && attrs.widget === "selection") { attrs.widget = undefined; } - var Obj = core.search_widgets_registry.getAny([attrs.widget, field.type]); if (Obj) { self.search_fields.push(new (Obj) (filter.item, field, self)); @@ -738,11 +797,15 @@ var SearchView = Widget.extend({ updateFilters: function (newFilters, filtersToRemove) { var self = this; var addedFilters = _.map(newFilters, function (filter) { + var domain = filter.domain; + if (domain instanceof Array) { + domain = Domain.prototype.arrayToString(domain); + } filter = { - attrs: {domain: filter.domain, help: filter.help}, + attrs: {domain: domain, help: filter.help}, }; var filterWidget = new search_inputs.Filter(filter); - var filterGroup = new search_inputs.FilterGroup([filterWidget], self, self.intervalMapping); + var filterGroup = new search_inputs.FilterGroup([filterWidget], self, self.intervalMapping, self.periodMapping); var facet = filterGroup.make_facet([filterGroup.make_value(filter)]); self.query.add([facet], {silent: true}); @@ -790,14 +853,23 @@ var SearchView = Widget.extend({ group.filters.forEach(function (filter) { if (!filter.attrs.invisible) { var filterId = _.uniqueId('__filter__'); + var isPeriod = filter.attrs.isPeriod; + var defaultPeriod = filter.attrs.default_period; filters.push({ - isActive: !!self.search_defaults[filter.attrs.name], - description: filter.attrs.string || filter.attrs.help || filter.attrs.name || filter.attrs.domain || 'Ω', itemId: filterId, + description: filter.attrs.string || filter.attrs.help || + filter.attrs.name || filter.attrs.domain || 'Ω', domain: filter.attrs.domain, + fieldName: filter.attrs.date, + isPeriod: isPeriod, + currentOptionId: defaultPeriod, + isActive: !self.hasFavorites && !!self.search_defaults[filter.attrs.name], groupId: groupId, }); self.filtersMapping.push({filterId: filterId, filter: filter, groupId: groupId}); + if (isPeriod) { + self.periodMapping.push({filter: filter, period: defaultPeriod, type: filter.attrs.type}); + } } }); self.groupsMapping.push({groupId: groupId, group: group, category: 'Filters'}); @@ -821,20 +893,22 @@ var SearchView = Widget.extend({ var groupId = _.uniqueId('__group__'); group.filters.forEach(function (groupby) { if (!groupby.attrs.invisible) { - var fieldName = groupby.attrs.fieldName; - var defaultOptionId = groupby.attrs.defaultInterval; var groupbyId = _.uniqueId('__groupby__'); + var fieldName = groupby.attrs.fieldName; + var isDate = groupby.attrs.isDate; + var defaultInterval = groupby.attrs.defaultInterval || DEFAULT_INTERVAL; groupbys.push({ - isActive: !!self.search_defaults[groupby.attrs.name], - description: groupby.attrs.string, itemId: groupbyId, + description: groupby.attrs.string || groupby.attrs.help || groupby.attrs.name + || groupby.attrs.fieldName || 'Ω', + isDate: isDate, fieldName: fieldName, + currentOptionId: defaultInterval, + isActive: !self.hasFavorites && !!self.search_defaults[groupby.attrs.name], groupId: groupId, - defaultOptionId: defaultOptionId, }); - if (self._fieldIsDate(fieldName)) { - self.intervalMapping.push({groupby: groupby, interval: defaultOptionId || 'month'}); - + if (isDate) { + self.intervalMapping.push({groupby: groupby, interval: defaultInterval}); } self.groupbysMapping.push({groupbyId: groupbyId, groupby: groupby, groupId: groupId}); } @@ -844,15 +918,6 @@ var SearchView = Widget.extend({ }); return new GroupByMenu(this, groupbys, this.fields); }, - - - _fieldIsDate: function (fieldName) { - if (_.contains(['date', 'datetime'], this.fields[fieldName].type)) { - return true; - } - return false; - }, - /** * Processes a fieldsView in place. In particular, parses its arch. * @@ -869,14 +934,20 @@ var SearchView = Widget.extend({ return fv; }, /** + * This function ask the groupby menu to deactive all groupbys with + * corresponding groupId in the list selectedGroupIds. + * (TO DO: simplify method... actually there is only one group of groupbys) + * * @private - * @param {string[]]} selectedGroupIds + * @param {string[]]} selectedGroupIds list of group ids to deactivate */ _unsetUnusedGroupbys: function (selectedGroupIds) { - var groupIds = this.selectedGroupIds.groupByCategory.reduce( - function (acc, id) { - if (!_.contains(selectedGroupIds, id)) { - acc.push(id); + var groupIds = this.groupsMapping.reduce( + function (acc, triple) { + if (triple.category === 'Group By') { + if (!_.contains(selectedGroupIds, triple.groupId)) { + acc.push(triple.groupId); + } } return acc; }, @@ -886,14 +957,19 @@ var SearchView = Widget.extend({ this.groupby_menu.unsetGroups(groupIds); }, /** + * This function ask the filters menu to deactive all filters with + * corresponding groupId in the list selectedGroupIds. + * * @private * @param {string[]]} selectedGroupIds */ _unsetUnusedFilters: function (selectedGroupIds) { - var groupIds = this.selectedGroupIds.filterCategory.reduce( - function (acc, id) { - if (!_.contains(selectedGroupIds, id)) { - acc.push(id); + var groupIds = this.groupsMapping.reduce( + function (acc, triple) { + if (triple.category === 'Filters') { + if (!_.contains(selectedGroupIds, triple.groupId)) { + acc.push(triple.groupId); + } } return acc; }, @@ -909,6 +985,11 @@ var SearchView = Widget.extend({ /** * + * this function is called in response to an event 'on_item_toggled'. + * this kind of event is triggered by the filters menu or the groupby menu + * when a user has clicked on a item (a filter or a groupby). + * The query is modified accordingly to the new state (active or not) of that item + * * @private * @param {OdooEvent} event */ @@ -927,27 +1008,36 @@ var SearchView = Widget.extend({ if (event.data.category === 'filterCategory') { var filter = _.findWhere(this.filtersMapping, {filterId: event.data.itemId}).filter; group = _.findWhere(this.groupsMapping, {groupId: event.data.groupId}).group; + if (event.data.optionId) { + var period = event.data.optionId; + _.findWhere(this.periodMapping, {filter: filter}).period = period; + group.updatePeriodMapping(this.periodMapping); + } group.toggle(filter); } }, /** + * this function is called when a new groupby has been added to the groupby menu + * via the 'Add Custom Groupby' submenu. The query is modified with the new groupby + * added to it as active. The communication betwenn the groupby menu and the search view + * is maintained by properly updating the mappings. * @private * @param {OdooEvent} event */ _onNewGroupby: function (event) { + var isDate = event.data.isDate; var attrs = { - context:"{'group_by':'" + event.data.fieldName + "''}", - name: event.data.description, - fieldName: event.data.fieldName, - }; - + context:"{'group_by':'" + event.data.fieldName + "''}", + name: event.data.description, + fieldName: event.data.fieldName, + isDate: isDate, + }; var groupby = new search_inputs.Filter({attrs: attrs}, this); if (event.data.optionId) { var interval = event.data.optionId; this.intervalMapping.push({groupby: groupby, interval: interval}); } - var group = new search_inputs.FilterGroup([groupby], this, this.intervalMapping); - group.toggle(groupby); + var group = new search_inputs.FilterGroup([groupby], this, this.intervalMapping, this.periodMapping); this.groupbysMapping.push({ groupbyId: event.data.itemId, groupby: groupby, @@ -958,44 +1048,49 @@ var SearchView = Widget.extend({ group: group, category: 'Group By', }); + group.toggle(groupby); }, - /** + /** + * this function is called when a new filter has been added to the filters menu + * via the 'Add Custom Filter' submenu. The query is modified with the new filter + * added to it as active. The communication betwenn the filters menu and the search view + * is maintained by properly updating the mappings. * @private * @param {OdooEvent} event */ - _onNewFilter: function (event) { + _onNewFilters: function (event) { var self= this; var filter; var filters = []; var groupId; _.each(event.data, function (filterDescription) { - filter = new search_inputs.Filter(filterDescription.filter, this); - filters.push(filter); - self.filtersMapping.push({ - filterId: filterDescription.itemId, - filter: filter, - groupId: filterDescription.groupId, - }); - // filters belong to the same group - if (!groupId) { - groupId = filterDescription.groupId; - } + filter = new search_inputs.Filter(filterDescription.filter, this); + filters.push(filter); + self.filtersMapping.push({ + filterId: filterDescription.itemId, + filter: filter, + groupId: filterDescription.groupId, }); - var group = new search_inputs.FilterGroup(filters, this, this.intervalMapping); + // filters belong to the same group + if (!groupId) { + groupId = filterDescription.groupId; + } + }); + var group = new search_inputs.FilterGroup(filters, this, this.intervalMapping, this.periodMapping); filters.forEach(function (filter) { group.toggle(filter, {silent: true}); }); - this.query.trigger('reset'); - - this.groupsMapping.push({ groupId: groupId, group: group, category: 'Filters', }); + this.query.trigger('reset'); }, /** + * this function is called when the option related to an item (filter or groupby) has been + * changed by the user. The query is modified appropriately. * * @private * @param {OdooEvent} event @@ -1012,8 +1107,11 @@ var SearchView = Widget.extend({ } if (event.data.category === 'filterCategory') { var filter = _.findWhere(this.filtersMapping, {filterId: event.data.itemId}).filter; + var period = event.data.optionId; + _.findWhere(this.periodMapping, {filter: filter}).period = period; group = _.findWhere(this.groupsMapping, {groupId: event.data.groupId}).group; - group.toggle(filter); + group.updatePeriodMapping(this.periodMapping); + this.query.trigger('reset'); } }, }); diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index 55f9a2e85d7..ed0adf0ff3b 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -7,7 +7,7 @@ var Dialog = require('web.Dialog'); var dom = require('web.dom'); var ListController = require('web.ListController'); var ListView = require('web.ListView'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var SearchView = require('web.SearchView'); var view_registry = require('web.view_registry'); @@ -293,7 +293,7 @@ var SelectCreateDialog = ViewDialog.extend({ var user_context = this.getSession().user_context; var _super = this._super.bind(this); - var context = pyeval.eval_domains_and_contexts({ + var context = pyUtils.eval_domains_and_contexts({ domains: [], contexts: [user_context, this.context] }).context; @@ -397,7 +397,7 @@ var SelectCreateDialog = ViewDialog.extend({ }); }, _process_search_data: function (domains, contexts, groupbys) { - var results = pyeval.eval_domains_and_contexts({ + var results = pyUtils.eval_domains_and_contexts({ domains: [this.domain].concat(domains), contexts: [this.context].concat(contexts), group_by_seq: groupbys || [], diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js index 68c8b23af13..8a09f7fa034 100644 --- a/addons/web/static/src/js/widgets/data_export.js +++ b/addons/web/static/src/js/widgets/data_export.js @@ -6,7 +6,7 @@ var crash_manager = require('web.crash_manager'); var data = require('web.data'); var Dialog = require('web.Dialog'); var framework = require('web.framework'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var QWeb = core.qweb; var _t = core._t; @@ -467,7 +467,7 @@ var DataExport = Dialog.extend({ fields: exported_fields, ids: this.ids_to_export, domain: this.domain, - context: pyeval.eval('contexts', [this.record.getContext()]), + context: pyUtils.eval('contexts', [this.record.getContext()]), import_compat: !!this.$import_compat_radios.filter(':checked').val(), })}, complete: framework.unblockUI, diff --git a/addons/web/static/src/js/widgets/dropdown_menu.js b/addons/web/static/src/js/widgets/dropdown_menu.js index d7e4bff32a4..335eed80e0e 100644 --- a/addons/web/static/src/js/widgets/dropdown_menu.js +++ b/addons/web/static/src/js/widgets/dropdown_menu.js @@ -6,33 +6,39 @@ var Widget = require('web.Widget'); var QWeb = core.qweb; -// interface IItem { -// itemId: string; -// description: string; -// groupId: string; -// isActive: boolean; (optional) -// isOpen: boolean; (optional) -// isRemovable: boolean; (optional) -// options: array of objects with 'optionId' and 'description' keys; (optional) -// defaultOptionId: string refers to an optionId (optional) -// currentOptionId: string refers to an optionId that is activated if item is active (optional) -// } + var DropdownMenu = Widget.extend({ template: 'web.DropdownMenu', events: { 'click li.o_menu_item': '_onItemClick', 'click li.o_item_option': '_onOptionClick', - 'click li.o_menu_item span.o_submenu_switcher': '_openItemOptions', 'click span.o_trash_button': '_onTrashClick', 'hidden.bs.dropdown': '_onBootstrapClose', }, /** + * override + * * @param {Widget} parent - * @param {Object} dropdowHeader object with keys 'title' and 'icon' - * @param {Object} items values are arrays of items (see interface IItems) - * @param {Object} itemGenerator + * @param {Object} dropdowHeader object used to customize the dropdown menu. The keys: + * - 'title' (e.g. 'Group By') + * - 'icon' (e.g. 'fa-bars') + * - 'symbol' (e.g. 'caret') + * - 'category' (describes the type of items) + * - 'style' (button style) + * @param {Object} items list of menu items (type IGMenuItem below) + * interface IMenuItem { + * itemId: string; (optional) unique id associated with the item + * description: string; label printed on screen + * groupId: string; + * isActive: boolean; (optional) determines if the item is considered active + * isOpen: boolean; (optional) in case there are options the submenu presenting the options + * is opened or closed according to isOpen + * isRemovable: boolean; (optional) can be removed from menu + * options: array of objects with 'optionId' and 'description' keys; (optional) + * currentOptionId: string refers to an optionId that is activated if item is active (optional) + * } */ init: function (parent, dropdownHeader, items) { this._super(parent); @@ -49,7 +55,9 @@ var DropdownMenu = Widget.extend({ this.items = _.sortBy(items, 'groupId'); _.each(this.items, this._prepareItem.bind(this)); }, - + /** + * override + */ start: function () { this.$menu = this.$('.o_dropdown_menu'); }, @@ -93,7 +101,6 @@ var DropdownMenu = Widget.extend({ }; this.trigger_up('menu_item_deleted', eventData); }, - /** * @private */ @@ -103,17 +110,21 @@ var DropdownMenu = Widget.extend({ this.$('ul.o_dropdown_menu').prepend($(newMenuItems)); }, /** + * this function computes some default values for some of the item keys + * according to values passed at initialization + * * @private * @param {Object} item */ _prepareItem: function (item) { + item.itemId = item.itemId || _.uniqueId('__item__'); item.isOpen = item.isOpen || false; item.isRemovable = item.isRemovable || false; - item.hasOptions = item.options && item.options.length !== 0 ? true : false; - item.defaultOptionId = item.hasOptions ? - (item.defaultOptionId || item.options[0].optionId) : - false; - item.currentOptionId = item.isActive ? (item.currentOptionId || item.defaultOptionId ) : false; + if (item.options && item.options.length !== 0) { + item.options = _.sortBy(item.options, 'groupId'); + item.hasOptions = true; + item.currentOptionId = item.isActive && item.currentOptionId ? item.currentOptionId : false; + } }, /** * @private @@ -121,23 +132,21 @@ var DropdownMenu = Widget.extend({ */ _toggleMenuItem: function (itemId) { var item = _.findWhere(this.items, {itemId: itemId}); - item.isActive = !item.isActive; - var eventData = { - category: this.dropdownCategory, - itemId: itemId, - isActive: item.isActive, - groupId: item.groupId, - }; + if (!item.hasOptions) { + item.isActive = !item.isActive; + var eventData = { + category: this.dropdownCategory, + itemId: itemId, + isActive: item.isActive, + groupId: item.groupId, + }; + this._renderMenuItems(); + this.trigger_up('menu_item_toggled', eventData); + } if (item.hasOptions) { - if (item.isActive) { - item.currentOptionId = item.defaultOptionId; - eventData.optionId = item.currentOptionId; - } else { - item.currentOptionId = false; - } + item.isOpen = !item.isOpen; + this._renderMenuItems(); } - this._renderMenuItems(); - this.trigger_up('menu_item_toggled', eventData); }, /** * @private @@ -168,19 +177,11 @@ var DropdownMenu = Widget.extend({ this.trigger_up('item_option_changed', eventData); } }, - /** - * @private - * @param {string} itemId - */ - _toggleMenuOptions: function (itemId) { - var item = _.findWhere(this.items, {itemId: itemId}); - item.isOpen = !item.isOpen; - this._renderMenuItems(); - }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- + /** * This method is called when Bootstrap trigger a 'close' event (for example * when the user clicks outside the dropdown menu). @@ -224,20 +225,8 @@ var DropdownMenu = Widget.extend({ var itemId = $(event.currentTarget).data('id'); this._removeItem(itemId); }, - /** - * @private - * @param {MouseEvent} event - */ - _openItemOptions: function (event) { - // we preventdefault the event here to avoid changing the hash # in the - // url. - event.preventDefault(); - event.stopPropagation(); - var itemId = $(event.currentTarget).data('id'); - this._toggleMenuOptions(itemId); - }, }); return DropdownMenu; -});
\ No newline at end of file +}); |
