From c71148cc70aacaf42b64fcda14837eeb4f153c41 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Mon, 11 Jun 2018 14:28:25 +0200 Subject: [PATCH 01/11] [DOC] web: amelioration of inline documentation --- .../src/js/views/search/search_inputs.js | 8 ++ .../static/src/js/views/search/search_view.js | 92 ++++++++++++------- .../static/src/js/widgets/dropdown_menu.js | 43 +++++---- .../static/tests/views/search_view_tests.js | 2 +- .../tests/widgets/dropdown_menu_tests.js | 34 ++++--- 5 files changed, 110 insertions(+), 69 deletions(-) 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..00d4d6281a0 100644 --- a/addons/web/static/src/js/views/search/search_inputs.js +++ b/addons/web/static/src/js/views/search/search_inputs.js @@ -514,6 +514,9 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ * * @param {Array} 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) { // If all filters are group_by and we're not initializing a GroupbyGroup, @@ -701,6 +704,11 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ }; })); }, + /* + * private + * + * @param {Object} intervalMapping + */ updateIntervalMapping: function (intervalMapping) { this.intervalMapping = intervalMapping; } 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..098df30c46a 100644 --- a/addons/web/static/src/js/views/search/search_view.js +++ b/addons/web/static/src/js/views/search/search_view.js @@ -187,6 +187,13 @@ var FacetView = Widget.extend({ } } }, + /* + * @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') + */ init: function (parent, model, intervalMapping) { this._super(parent); this.model = model; @@ -218,6 +225,13 @@ var FacetView = Widget.extend({ var FacetValueView = Widget.extend({ template: 'SearchView.FacetView.Value', + /* + * @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, interval) { this._super(parent); this.model = model; @@ -275,10 +289,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_filter: '_onNewFilter', }, defaults: _.extend({}, Widget.prototype.defaults, { hidden: false, @@ -832,7 +846,7 @@ var SearchView = Widget.extend({ groupId: groupId, defaultOptionId: defaultOptionId, }); - if (self._fieldIsDate(fieldName)) { + if (_.contains(['date', 'datetime'], self.fields[fieldName].type)) { self.intervalMapping.push({groupby: groupby, interval: defaultOptionId || 'month'}); } @@ -844,15 +858,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,8 +874,12 @@ 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( @@ -886,6 +895,9 @@ 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 */ @@ -908,6 +920,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 @@ -931,16 +948,19 @@ var SearchView = Widget.extend({ } }, /** + * 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 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, + }; var groupby = new search_inputs.Filter({attrs: attrs}, this); if (event.data.optionId) { var interval = event.data.optionId; @@ -959,7 +979,11 @@ var SearchView = Widget.extend({ category: 'Group By', }); }, - /** + /** + * 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 */ @@ -970,25 +994,23 @@ var SearchView = Widget.extend({ 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, }); + // filters belong to the same group + if (!groupId) { + groupId = filterDescription.groupId; + } + }); var group = new search_inputs.FilterGroup(filters, this, this.intervalMapping); filters.forEach(function (filter) { group.toggle(filter, {silent: true}); }); this.query.trigger('reset'); - - this.groupsMapping.push({ groupId: groupId, group: group, @@ -996,6 +1018,8 @@ var SearchView = Widget.extend({ }); }, /** + * 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 diff --git a/addons/web/static/src/js/widgets/dropdown_menu.js b/addons/web/static/src/js/widgets/dropdown_menu.js index d7e4bff32a4..2999d09bbf9 100644 --- a/addons/web/static/src/js/widgets/dropdown_menu.js +++ b/addons/web/static/src/js/widgets/dropdown_menu.js @@ -6,17 +6,7 @@ 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', @@ -29,10 +19,28 @@ var DropdownMenu = Widget.extend({ }, /** + * 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; 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) + * defaultOptionId: string refers to an optionId (optional) + * currentOptionId: string refers to an optionId that is activated if item is active (optional) + * } */ init: function (parent, dropdownHeader, items) { this._super(parent); @@ -93,7 +101,6 @@ var DropdownMenu = Widget.extend({ }; this.trigger_up('menu_item_deleted', eventData); }, - /** * @private */ @@ -103,6 +110,9 @@ 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 */ @@ -181,6 +191,7 @@ var DropdownMenu = Widget.extend({ //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- + /** * This method is called when Bootstrap trigger a 'close' event (for example * when the user clicks outside the dropdown menu). @@ -240,4 +251,4 @@ var DropdownMenu = Widget.extend({ return DropdownMenu; -}); \ No newline at end of file +}); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index 391b3ed46f5..e4db69131f7 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -296,4 +296,4 @@ QUnit.module('Search View', { actionManager.destroy(); }); }); -}); \ No newline at end of file +}); diff --git a/addons/web/static/tests/widgets/dropdown_menu_tests.js b/addons/web/static/tests/widgets/dropdown_menu_tests.js index ae8426c5b4c..92444c57075 100644 --- a/addons/web/static/tests/widgets/dropdown_menu_tests.js +++ b/addons/web/static/tests/widgets/dropdown_menu_tests.js @@ -13,25 +13,24 @@ function createDropdownMenu(dropdownTitle, groups, params) { return menu; } - QUnit.module('Web', { beforeEach: function () { this.items = [ - { - isActive: false, - isOpen: false, - description: 'Some Item', - itemId: 1, - groupId: 1, - }, - { - isActive: true, - description: 'Some other Item', - itemId: 2, - options: [], - isRemovable: true, - groupId: 2, - }, + { + isActive: false, + isOpen: false, + description: 'Some Item', + itemId: 1, + groupId: 1, + }, + { + isActive: true, + description: 'Some other Item', + itemId: 2, + options: [], + isRemovable: true, + groupId: 2, + }, ]; this.dropdownHeader = { title: "Menu", @@ -196,6 +195,5 @@ QUnit.module('Web', { assert.strictEqual(dropdownMenu.$('li').length, 2); dropdownMenu.destroy(); }); - }); -}); \ No newline at end of file +}); From 64f3580af7c93abe3402493b2ab24d99d00fa584 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Fri, 15 Jun 2018 09:23:46 +0200 Subject: [PATCH 02/11] [IMP] web: add support of modulo in pyeval --- addons/web/static/lib/py.js/lib/py.js | 6 ++++++ addons/web/static/tests/core/pyeval_tests.js | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/addons/web/static/lib/py.js/lib/py.js b/addons/web/static/lib/py.js/lib/py.js index f4ae13d9e44..06d834321de 100644 --- a/addons/web/static/lib/py.js/lib/py.js +++ b/addons/web/static/lib/py.js/lib/py.js @@ -862,6 +862,12 @@ var py = {}; } return py.float.fromJSON(this._value + other._value); }, + __mod__: function (other) { + if (!py.PY_isInstance(other, py.float)) { + return py.NotImplemented; + } + return py.float.fromJSON(this._value % other._value); + }, __neg__: function () { return py.float.fromJSON(-this._value); }, diff --git a/addons/web/static/tests/core/pyeval_tests.js b/addons/web/static/tests/core/pyeval_tests.js index 737466c8c93..95d8783ad96 100644 --- a/addons/web/static/tests/core/pyeval_tests.js +++ b/addons/web/static/tests/core/pyeval_tests.js @@ -18,6 +18,16 @@ QUnit.module('core', function () { assert.strictEqual(result, 42, "should properly evaluate basic sum"); }); + QUnit.test('simple arithmetic', function(assert) { + assert.expect(2); + + var result = pyEval.py_eval("1 + 2"); + assert.strictEqual(result, 3, "should properly evaluate sum"); + result = pyEval.py_eval("42 % 5"); + assert.strictEqual(result, 2, "should properly evaluate modulo operator"); + }); + + QUnit.test('not prefix', function (assert) { assert.expect(3); From dcc4d716234349c71f51edbd621963b5210c65de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 22 Jun 2018 10:14:05 +0200 Subject: [PATCH 03/11] [REF] web: move some code from pyeval to lib py_extras Some functions that were in web.pyeval were actually "odoo independant". To make them easier to manage/understand, this commit move them in the library py.js, as an extra file. --- addons/web/static/lib/py.js/lib/py_extras.js | 784 +++++++++++++++++++ addons/web/static/src/js/core/pyeval.js | 752 +----------------- addons/web/static/src/js/core/utils.js | 25 - addons/web/views/webclient_templates.xml | 1 + 4 files changed, 801 insertions(+), 761 deletions(-) create mode 100644 addons/web/static/lib/py.js/lib/py_extras.js diff --git a/addons/web/static/lib/py.js/lib/py_extras.js b/addons/web/static/lib/py.js/lib/py_extras.js new file mode 100644 index 00000000000..2bc69ece598 --- /dev/null +++ b/addons/web/static/lib/py.js/lib/py_extras.js @@ -0,0 +1,784 @@ +(function (py) { +"use strict"; + +/** + * This file add extra functionality to the python interpreter exported by py.js + * + * Main extra functionality is about time management, more precisely: + * - date + * - datetime + * - relativedelta + * + * These python modules are exported in the py.extras object, and can be added + * to the evaluation context. For example, + * + * var context = { + * datetime: py.extras.datetime, + * date: py.extras.date, + * time: py.extras.time, + * }; + * var result = py.eval(some_python_expression, context); + */ + +/* + * py.js helpers and setup + */ + +/** + * computes (Math.floor(a/b), a%b and passes that to the callback. + * + * returns the callback's result + */ +function divmod (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); +} + +/** + * Passes the fractional and integer parts of x to the callback, returns + * the callback's result + */ +function modf(x, fn) { + var mod = x%1; + if (mod < 0) { + mod += 1; + } + return fn(mod, Math.floor(x)); +} + +function assert(bool) { + if (!bool) { + throw new Error("AssertionError"); + } +} + + +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 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; + divmod(n, DI400Y, function (_n400, n) { + n400 = _n400; + divmod(n, DI100Y, function (_n100, n) { + n100 = _n100; + divmod(n, DI4Y, function (_n4, n) { + n4 = _n4; + 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) { + assert(n0 === 0); + return { + year: year - 1, + month: 12, + day: 31 + }; + } + + var leapyear = n1 === 3 && (n4 !== 24 || n100 == 3); + 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) { + divmod(microsecond, 1000000, function (carry, ms) { + microsecond = ms; + second += carry; + }); + } + if (second < 0 || second > 59) { + divmod(second, 60, function (carry, s) { + second = s; + minute += carry; + }); + } + if (minute < 0 || minute > 59) { + divmod(minute, 60, function (carry, m) { + minute = m; + hour += carry; + }); + } + if (hour < 0 || hour > 23) { + 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) { + 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 . + var daysecondsfrac = modf(days, function (dayfrac, days) { + d = days; + if (dayfrac) { + return modf(dayfrac * 24 * 3600, function (dsf, dsw) { + s = dsw; + return dsf; + }); + } + return 0; + }); + + var secondsfrac = modf(seconds, function (sf, s) { + seconds = s; + return sf + daysecondsfrac; + }); + divmod(seconds, 24*3600, function (days, seconds) { + d += days; + s += seconds; + }); + // seconds isn't referenced again before redefinition + + microseconds += secondsfrac * 1e6; + divmod(microseconds, 1000000, function (seconds, microseconds) { + 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; + divmod(this.seconds, 60, function (m, s) { + 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()]); + }), +}); + +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 11) { + var s = months > 0 ? 1 : -1; + var r = 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 + }); + } +}); + +py.extras = { + datetime: datetime, + time: time, + relativedelta: relativedelta, +}; + +})(typeof exports === 'undefined' ? py : exports); diff --git a/addons/web/static/src/js/core/pyeval.js b/addons/web/static/src/js/core/pyeval.js index ee4ac697fd9..47d6d07bf64 100644 --- a/addons/web/static/src/js/core/pyeval.js +++ b/addons/web/static/src/js/core/pyeval.js @@ -2,741 +2,9 @@ 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 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 . - 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 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 @@ -951,15 +219,27 @@ function eval_groupbys(contexts, evaluation_context) { 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()]); +} + function pycontext() { return { - datetime: datetime, + datetime: py.extras.datetime, context_today: context_today, - time: time, - relativedelta: relativedelta, + time: py.extras.time, + relativedelta: py.extras.relativedelta, current_date: py.PY_call( - time.strftime, [py.str.fromJSON('%Y-%m-%d')]), + py.extras.time.strftime, [py.str.fromJSON('%Y-%m-%d')]), }; } 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 @@ -51,20 +51,6 @@ var utils = { confine: function (val, min, max) { 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 @@ -289,17 +275,6 @@ var utils = { str = "" + str; 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 diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index e4c28872b64..ed40fb36625 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -173,6 +173,7 @@ + From eed4d586a026544facd35e3ca5184ef7f609c562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 22 Jun 2018 10:26:40 +0200 Subject: [PATCH 04/11] [REF] *: rename web.pyeval into web.py_utils The pyeval name is quite confusing, given that there is already an eval and an evaluate function in py.js, and that the web.pyeval exports two evaluate functions. Since this file is more about odoo specific customization to py.js, it is renamed in py_utils to better express its intended use. --- addons/board/static/src/js/favorite_menu.js | 6 +- .../static/src/js/search.js | 4 +- addons/mail/static/src/js/discuss.js | 4 +- .../static/src/js/timesheet_plan.js | 4 +- addons/web/static/src/js/_deprecated/data.js | 16 +-- .../static/src/js/chrome/action_manager.js | 6 +- .../js/chrome/action_manager_act_window.js | 10 +- addons/web/static/src/js/chrome/sidebar.js | 4 +- addons/web/static/src/js/core/context.js | 4 +- addons/web/static/src/js/core/domain.js | 4 +- .../src/js/core/{pyeval.js => py_utils.js} | 2 +- .../static/src/js/views/basic/basic_view.js | 4 +- .../src/js/views/list/list_controller.js | 4 +- .../src/js/views/search/favorites_menu.js | 4 +- .../src/js/views/search/search_inputs.js | 8 +- .../static/src/js/views/search/search_view.js | 4 +- .../web/static/src/js/views/view_dialogs.js | 6 +- .../web/static/src/js/widgets/data_export.js | 4 +- .../{pyeval_tests.js => py_utils_tests.js} | 108 +++++++++--------- .../web/static/tests/helpers/mock_server.js | 6 +- addons/web/static/tests/views/form_tests.js | 4 +- addons/web/views/webclient_templates.xml | 4 +- 22 files changed, 110 insertions(+), 110 deletions(-) rename addons/web/static/src/js/core/{pyeval.js => py_utils.js} (99%) rename addons/web/static/tests/core/{pyeval_tests.js => py_utils_tests.js} (93%) diff --git a/addons/board/static/src/js/favorite_menu.js b/addons/board/static/src/js/favorite_menu.js index 112b4d634fa..cdb531914c9 100644 --- a/addons/board/static/src/js/favorite_menu.js +++ b/addons/board/static/src/js/favorite_menu.js @@ -6,7 +6,7 @@ var Context = require('web.Context'); var core = require('web.core'); var Domain = require('web.Domain'); var FavoriteMenu = require('web.FavoriteMenu'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var _t = core._t; var QWeb = core.qweb; @@ -64,7 +64,7 @@ FavoriteMenu.include({ }); context.add({ - group_by: pyeval.eval('groupbys', search_data.groupbys || []) + group_by: pyUtils.eval('groupbys', search_data.groupbys || []) }); // AAB: trigger_up an event that will be intercepted by the controller, // as soon as the controller is the parent of the control panel @@ -77,7 +77,7 @@ FavoriteMenu.include({ var currentAction = am.getCurrentAction(); var controller = am.getCurrentController(); context.add(controller.widget.getContext()); - var c = pyeval.eval('context', context); + var c = pyUtils.eval('context', context); for (var k in c) { if (c.hasOwnProperty(k) && /^search_default_/.test(k)) { delete c[k]; diff --git a/addons/google_spreadsheet/static/src/js/search.js b/addons/google_spreadsheet/static/src/js/search.js index 20b7c804f33..2177407520b 100644 --- a/addons/google_spreadsheet/static/src/js/search.js +++ b/addons/google_spreadsheet/static/src/js/search.js @@ -6,7 +6,7 @@ var core = require('web.core'); var data = require('web.data'); var Domain = require('web.Domain'); var FavoriteMenu = require('web.FavoriteMenu'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var QWeb = core.qweb; @@ -30,7 +30,7 @@ FavoriteMenu.include({ list_view = _.findWhere(controller.widget.actionViews, {type: 'list'}), list_view_id = list_view ? list_view.viewID : false, domain = [], - groupbys = pyeval.eval('groupbys', sv_data.groupbys).join(" "), + groupbys = pyUtils.eval('groupbys', sv_data.groupbys).join(" "), ds = new data.DataSet(this, 'google.drive.config'); _.each(sv_data.domains, function (d) { diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js index ba0ad31a849..47af7ca3f08 100644 --- a/addons/mail/static/src/js/discuss.js +++ b/addons/mail/static/src/js/discuss.js @@ -12,7 +12,7 @@ var core = require('web.core'); var data = require('web.data'); var Dialog = require('web.Dialog'); var dom = require('web.dom'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var SearchView = require('web.SearchView'); var session = require('web.session'); @@ -1320,7 +1320,7 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, { _onSearch: function (ev) { ev.stopPropagation(); var session = this.getSession(); - var result = pyeval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ domains: ev.data.domains, contexts: [session.user_context], }); diff --git a/addons/sale_timesheet/static/src/js/timesheet_plan.js b/addons/sale_timesheet/static/src/js/timesheet_plan.js index 6596bd4ea88..afebfac23fe 100644 --- a/addons/sale_timesheet/static/src/js/timesheet_plan.js +++ b/addons/sale_timesheet/static/src/js/timesheet_plan.js @@ -5,7 +5,7 @@ var AbstractAction = require('web.AbstractAction'); var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); var data = require('web.data'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var SearchView = require('web.SearchView'); var _t = core._t; @@ -297,7 +297,7 @@ var ProjectPlan = AbstractAction.extend(ControlPanelMixin, { event.stopPropagation(); var session = this.getSession(); // group by are disabled, so we don't take care of them - var result = pyeval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ domains: event.data.domains, contexts: [session.user_context].concat(event.data.contexts) }); 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> | 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..86e1f85a634 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 @@ -240,7 +240,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/pyeval.js b/addons/web/static/src/js/core/py_utils.js similarity index 99% rename from addons/web/static/src/js/core/pyeval.js rename to addons/web/static/src/js/core/py_utils.js index 47d6d07bf64..8590965cee2 100644 --- a/addons/web/static/src/js/core/pyeval.js +++ b/addons/web/static/src/js/core/py_utils.js @@ -1,4 +1,4 @@ -odoo.define('web.pyeval', function (require) { +odoo.define('web.py_utils', function (require) { "use strict"; var core = require('web.core'); 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..cb4afe73476 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({ @@ -222,7 +222,7 @@ var BasicView = AbstractView.extend({ }); 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..bd622797c49 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,7 @@ 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 pyUtils = require('web.py_utils'); var session = require('web.session'); var Widget = require('web.Widget'); @@ -105,7 +105,7 @@ return Widget.extend({ controllerContext = ctx; }, }); - var results = pyeval.eval_domains_and_contexts({ + var results = pyUtils.eval_domains_and_contexts({ domains: search.domains, contexts: [user_context].concat(search.contexts.concat(controllerContext || [])), group_by_seq: search.groupbys || [], 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 00d4d6281a0..d3f36254819 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, @@ -524,7 +524,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.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); } 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 098df30c46a..e5a00434745 100644 --- a/addons/web/static/src/js/views/search/search_view.js +++ b/addons/web/static/src/js/views/search/search_view.js @@ -7,7 +7,7 @@ var core = require('web.core'); 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'); @@ -683,7 +683,7 @@ 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]; 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/tests/core/pyeval_tests.js b/addons/web/static/tests/core/py_utils_tests.js similarity index 93% rename from addons/web/static/tests/core/pyeval_tests.js rename to addons/web/static/tests/core/py_utils_tests.js index 95d8783ad96..f53c4dca8f6 100644 --- a/addons/web/static/tests/core/pyeval_tests.js +++ b/addons/web/static/tests/core/py_utils_tests.js @@ -1,8 +1,8 @@ -odoo.define('web.pyEval_tests', function(require) { +odoo.define('web.py_utils_tests', function(require) { "use strict"; var Context = require('web.Context'); -var pyEval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var time = require('web.time'); QUnit.module('core', function () { @@ -12,9 +12,9 @@ QUnit.module('core', function () { QUnit.test('simple python expression', function(assert) { assert.expect(2); - var result = pyEval.py_eval("True and False"); + var result = pyUtils.py_eval("True and False"); assert.strictEqual(result, false, "should properly evaluate basic expression"); - result = pyEval.py_eval("a + b", {a: 1, b: 41}); + result = pyUtils.py_eval("a + b", {a: 1, b: 41}); assert.strictEqual(result, 42, "should properly evaluate basic sum"); }); @@ -38,7 +38,7 @@ QUnit.module('core', function () { function makeTimeCheck (assert) { - var context = pyEval.context(); + var context = pyUtils.context(); return function (expr, func, message) { // evaluate expr between two calls to new Date(), and check that // the result is between the transformed dates @@ -51,7 +51,7 @@ QUnit.module('core', function () { // Port from pypy/lib_pypy/test_datetime.py function makeEq(assert, c2) { - var ctx = pyEval.context(); + var ctx = pyUtils.context(); var c = _.extend({ td: ctx.datetime.timedelta }, c2 || {}); return function (a, b, message) { assert.ok(py.eval(a + ' == ' + b, c), message); @@ -81,7 +81,7 @@ QUnit.module('core', function () { QUnit.test('context_today', function (assert) { assert.expect(1); - var check = makeTimeCheck(assert, pyEval); + var check = makeTimeCheck(assert, pyUtils); check("context_today().strftime('%Y-%m-%d')", function(d) { return String(_.str.sprintf('%04d-%02d-%02d', @@ -119,7 +119,7 @@ QUnit.module('core', function () { QUnit.test('timedelta.test_computations', function (assert) { assert.expect(28); - var c = pyEval.context(); + var c = pyUtils.context(); var zero = py.float.fromJSON(0); var eq = makeEq(assert, { // one week @@ -174,7 +174,7 @@ QUnit.module('core', function () { QUnit.test('timedelta.test_basic_attributes', function (assert) { assert.expect(3); - var ctx = pyEval.context(); + var ctx = pyUtils.context(); assert.strictEqual(py.eval('datetime.timedelta(1, 7, 31).days', ctx), 1); assert.strictEqual(py.eval('datetime.timedelta(1, 7, 31).seconds', ctx), 7); assert.strictEqual(py.eval('datetime.timedelta(1, 7, 31).microseconds', ctx), 31); @@ -183,7 +183,7 @@ QUnit.module('core', function () { QUnit.test('timedelta.test_total_seconds', function (assert) { assert.expect(6); - var c = { timedelta: pyEval.context().datetime.timedelta }; + var c = { timedelta: pyUtils.context().datetime.timedelta }; assert.strictEqual(py.eval('timedelta(365).total_seconds()', c), 31536000); assert.strictEqual( py.eval('timedelta(seconds=123456.789012).total_seconds()', c), @@ -201,7 +201,7 @@ QUnit.module('core', function () { QUnit.test('timedelta.test_str', function (assert) { assert.expect(10); - var c = { td: pyEval.context().datetime.timedelta }; + var c = { td: pyUtils.context().datetime.timedelta }; assert.strictEqual(py.eval('str(td(1))', c), "1 day, 0:00:00"); assert.strictEqual(py.eval('str(td(-1))', c), "-1 day, 0:00:00"); @@ -228,7 +228,7 @@ QUnit.module('core', function () { assert.expect(3); var td = py.PY_call( - pyEval.context().datetime.timedelta, + pyUtils.context().datetime.timedelta, {microseconds: py.float.fromJSON(-1)}); assert.strictEqual(td.days, -1); assert.strictEqual(td.seconds, 24 * 3600 - 1); @@ -238,7 +238,7 @@ QUnit.module('core', function () { QUnit.test('timedelta.test_bool', function (assert) { assert.expect(5); - var c = { td: pyEval.context().datetime.timedelta }; + var c = { td: pyUtils.context().datetime.timedelta }; assert.ok(py.eval('bool(td(1))', c)); assert.ok(py.eval('bool(td(0, 1))', c)); assert.ok(py.eval('bool(td(0, 0, 1))', c)); @@ -249,7 +249,7 @@ QUnit.module('core', function () { QUnit.test('date.test_computations', function (assert) { assert.expect(31); - var d = pyEval.context().datetime; + var d = pyUtils.context().datetime; var a = d.date.fromJSON(2002, 1, 31); var b = d.date.fromJSON(1956, 1, 31); @@ -333,23 +333,23 @@ QUnit.module('core', function () { assert.strictEqual( py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')", - pyEval.context()), + pyUtils.context()), "2012-02-14 23:59:59"); assert.strictEqual( py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=1)).strftime('%Y-%m-%d')", - pyEval.context()), + pyUtils.context()), "2012-02-16"); assert.strictEqual( py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d')", - pyEval.context()), + pyUtils.context()), "2012-02-14"); assert.strictEqual( py.eval("(datetime.date(2012, 2, 1) + relativedelta(days=-1)).strftime('%Y-%m-%d')", - pyEval.context()), + pyUtils.context()), '2012-01-31'); assert.strictEqual( py.eval("(datetime.date(2015,2,5)+relativedelta(days=-6,weekday=0)).strftime('%Y-%m-%d')", - pyEval.context()), + pyUtils.context()), '2015-02-02'); }); @@ -358,19 +358,19 @@ QUnit.module('core', function () { assert.expect(4); assert.strictEqual( py.eval("(datetime.datetime(2017, 2, 15, 1, 7, 31) + datetime.timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')", - pyEval.context()), + pyUtils.context()), "2017-02-16 01:07:31"); assert.strictEqual( py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')", - pyEval.context()), + pyUtils.context()), "2012-02-15 00:07:31"); assert.strictEqual( py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(hours=-1)).strftime('%Y-%m-%d %H:%M:%S')", - pyEval.context()), + pyUtils.context()), "2012-02-15 00:07:31"); assert.strictEqual( py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(minutes=100)).strftime('%Y-%m-%d %H:%M:%S')", - pyEval.context()), + pyUtils.context()), "2012-02-15 02:47:31"); }); @@ -379,7 +379,7 @@ QUnit.module('core', function () { var result = py.eval( 'datetime.datetime(2012, 2, 15, 1, 7, 31)', - pyEval.context()); + pyUtils.context()); assert.ok(result instanceof Date); assert.strictEqual(result.getFullYear(), 2012); @@ -397,14 +397,14 @@ QUnit.module('core', function () { 'datetime.datetime.combine(datetime.date(2012, 2, 15),' + ' datetime.time(1, 7, 13))' + ' .strftime("%Y-%m-%d %H:%M:%S")', - pyEval.context()); + pyUtils.context()); assert.strictEqual(result, "2012-02-15 01:07:13"); result = py.eval( 'datetime.datetime.combine(datetime.date(2012, 2, 15),' + ' datetime.time())' + ' .strftime("%Y-%m-%d %H:%M:%S")', - pyEval.context()); + pyUtils.context()); assert.strictEqual(result, '2012-02-15 00:00:00'); }); @@ -415,7 +415,7 @@ QUnit.module('core', function () { 'datetime.datetime(2012, 2, 15, 1, 7, 13)' + ' .replace(hour=0, minute=0, second=0)' + ' .strftime("%Y-%m-%d %H:%M:%S")', - pyEval.context()); + pyUtils.context()); assert.strictEqual(result, "2012-02-15 00:00:00"); }); @@ -433,7 +433,7 @@ QUnit.module('core', function () { QUnit.test('empty, basic', function (assert) { assert.expect(3); - var result = pyEval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ contexts: [this.user_context], domains: [], }); @@ -480,7 +480,7 @@ QUnit.module('core', function () { }, { "active_id": 9, "active_ids": [ 9 ], "active_model": "mail.compose.message" } ]; - var result = pyEval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ contexts: ctx, domins: [], }); @@ -527,7 +527,7 @@ QUnit.module('core', function () { "__eval_context": null, "__ref": "compound_context" }]; - var result = pyEval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ contexts: ctx, domains: [], }); @@ -547,7 +547,7 @@ QUnit.module('core', function () { QUnit.test('domain with time', function (assert) { assert.expect(1); - var result = pyEval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ domains: [ [['type', '=', 'contract']], ["|", ["state", "in", ["open", "draft"]], [["type", "=", "contract"], ["state", "=", "pending"]]], @@ -581,13 +581,13 @@ QUnit.module('core', function () { __debug: "[('company_id', '=', context.get('company_id',False))]" }; - var result1 = pyEval.eval_domains_and_contexts({ + var result1 = pyUtils.eval_domains_and_contexts({ domains: [d], contexts: [], }); assert.deepEqual(result1.domain, [['company_id', '=', false]]); - var result2 = pyEval.eval_domains_and_contexts({ + var result2 = pyUtils.eval_domains_and_contexts({ domains: [d], contexts: [], eval_context: {company_id: 42}, @@ -602,7 +602,7 @@ QUnit.module('core', function () { var c = "{'default_opportunity_id': active_id, 'default_duration': 1.0, 'lng': lang}"; var cc = new Context(c); cc.set_eval_context({active_id: 42}); - var result = pyEval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ domains:[], contexts: [this.user_context, cc] }); @@ -620,7 +620,7 @@ QUnit.module('core', function () { assert.expect(1); var d = "[('state','!=','cancel'),('opening_date','>',context_today().strftime('%Y-%m-%d'))]"; - var result = pyEval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ domains: [d], contexts: [], }); @@ -638,7 +638,7 @@ QUnit.module('core', function () { assert.expect(1); var d = "[('type','=','in'),('day','<=', time.strftime('%Y-%m-%d')),('day','>',(context_today()-datetime.timedelta(days=15)).strftime('%Y-%m-%d'))]"; - var result = pyEval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ domains: [d], contexts: [], }); @@ -676,7 +676,7 @@ QUnit.module('core', function () { {"__ref": "context", "__debug": "{'default_ref': 'stock.picking.out,'+str(context.get('active_id', False))}", "__id": "54d6ad1d6c45"} ], "__eval_context": null} ]; - var result = pyEval.eval_domains_and_contexts({ + var result = pyUtils.eval_domains_and_contexts({ domains: [], contexts: cs, }); @@ -706,13 +706,13 @@ QUnit.module('core', function () { __debug: '{"foo": context.get("bar", "qux")}' }]; assert.deepEqual( - pyEval.eval('contexts', context_to_eval, {bar: "ok"}), + pyUtils.eval('contexts', context_to_eval, {bar: "ok"}), {foo: 'ok'}); assert.deepEqual( - pyEval.eval('contexts', context_to_eval, {bar: false}), + pyUtils.eval('contexts', context_to_eval, {bar: false}), {foo: false}); assert.deepEqual( - pyEval.eval('contexts', context_to_eval), + pyUtils.eval('contexts', context_to_eval), {foo: 'qux'}); }); @@ -722,7 +722,7 @@ QUnit.module('core', function () { // Context n should have base evaluation context + all of contexts // 0..n-1 in its own evaluation context var active_id = 4; - var result = pyEval.eval('contexts', [ + var result = pyUtils.eval('contexts', [ { "__contexts": [ { @@ -769,7 +769,7 @@ QUnit.module('core', function () { QUnit.test('non-literal_eval_contexts', function (assert) { assert.expect(1); - var result = pyEval.eval('contexts', [{ + var result = pyUtils.eval('contexts', [{ "__ref": "compound_context", "__contexts": [ {"__ref": "context", "__debug": "{'type':parent.type}", @@ -851,7 +851,7 @@ QUnit.module('core', function () { QUnit.test('return-input-value', function (assert) { assert.expect(1); - var result = pyEval.eval('contexts', [{ + var result = pyUtils.eval('contexts', [{ __ref: 'compound_context', __contexts: ["{'line_id': line_id , 'journal_id': journal_id }"], __eval_context: { @@ -921,9 +921,9 @@ QUnit.module('core', function () { assert.expect(1); var current_date = time.date_to_str(new Date()); - var result = pyEval.eval('domains', + var result = pyUtils.eval('domains', [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}], - pyEval.context()); + pyUtils.context()); assert.deepEqual(result, [ ['name', '>=', current_date], ['name', '<=', current_date] @@ -938,13 +938,13 @@ QUnit.module('core', function () { __debug: '[("foo", "=", context.get("bar", "qux"))]' }, [['bar', '>=', 42]]]; assert.deepEqual( - pyEval.eval('domains', domains_to_eval, {bar: "ok"}), + pyUtils.eval('domains', domains_to_eval, {bar: "ok"}), [['foo', '=', 'ok'], ['bar', '>=', 42]]); assert.deepEqual( - pyEval.eval('domains', domains_to_eval, {bar: false}), + pyUtils.eval('domains', domains_to_eval, {bar: false}), [['foo', '=', false], ['bar', '>=', 42]]); assert.deepEqual( - pyEval.eval('domains', domains_to_eval), + pyUtils.eval('domains', domains_to_eval), [['foo', '=', 'qux'], ['bar', '>=', 42]]); }); @@ -953,7 +953,7 @@ QUnit.module('core', function () { QUnit.test('groupbys_00', function (assert) { assert.expect(1); - var result = pyEval.eval('groupbys', [ + var result = pyUtils.eval('groupbys', [ {group_by: 'foo'}, {group_by: ['bar', 'qux']}, {group_by: null}, @@ -965,7 +965,7 @@ QUnit.module('core', function () { QUnit.test('groupbys_01', function (assert) { assert.expect(1); - var result = pyEval.eval('groupbys', [ + var result = pyUtils.eval('groupbys', [ {group_by: 'foo'}, { __ref: 'context', __debug: '{"group_by": "bar"}' }, {group_by: 'grault'} @@ -976,7 +976,7 @@ QUnit.module('core', function () { QUnit.test('groupbys_02', function (assert) { assert.expect(1); - var result = pyEval.eval('groupbys', [ + var result = pyUtils.eval('groupbys', [ {group_by: 'foo'}, { __ref: 'compound_context', @@ -991,7 +991,7 @@ QUnit.module('core', function () { QUnit.test('groupbys_03', function (assert) { assert.expect(1); - var result = pyEval.eval('groupbys', [ + var result = pyUtils.eval('groupbys', [ {group_by: 'foo'}, { __ref: 'compound_context', @@ -1008,7 +1008,7 @@ QUnit.module('core', function () { QUnit.test('groupbys_04', function (assert) { assert.expect(1); - var result = pyEval.eval('groupbys', [ + var result = pyUtils.eval('groupbys', [ {group_by: 'foo'}, { __ref: 'compound_context', @@ -1025,7 +1025,7 @@ QUnit.module('core', function () { QUnit.test('groupbys_05', function (assert) { assert.expect(1); - var result = pyEval.eval('groupbys', [ + var result = pyUtils.eval('groupbys', [ {group_by: 'foo'}, { __ref: 'context', __debug: '{"group_by": value}' }, {group_by: 'grault'} diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 5e82766b051..81d8ec91bfd 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -3,7 +3,7 @@ odoo.define('web.MockServer', function (require) { var Class = require('web.Class'); var Domain = require('web.Domain'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var MockServer = Class.extend({ /** @@ -235,7 +235,7 @@ var MockServer = Class.extend({ // 'transfer_node_to_modifiers' simulation var attrs = node.getAttribute('attrs'); if (attrs) { - attrs = pyeval.py_eval(attrs); + attrs = pyUtils.py_eval(attrs); _.extend(modifiers, attrs); } @@ -250,7 +250,7 @@ var MockServer = Class.extend({ var mod = node.getAttribute(a); if (mod) { var pyevalContext = window.py.dict.fromJSON(context || {}); - var v = pyeval.py_eval(mod, {context: pyevalContext}) ? true: false; + var v = pyUtils.py_eval(mod, {context: pyevalContext}) ? true: false; if (inTreeView && a === 'invisible') { modifiers.column_invisible = v; } else if (v || !(a in modifiers) || !_.isArray(modifiers[a])) { diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 4ffdd72b089..035c815ae95 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -9,7 +9,7 @@ var fieldRegistry = require('web.field_registry'); var FormView = require('web.FormView'); var mixins = require('web.mixins'); var NotificationService = require('web.NotificationService'); -var pyeval = require('web.pyeval'); +var pyUtils = require('web.py_utils'); var testUtils = require('web.test_utils'); var widgetRegistry = require('web.widget_registry'); var Widget = require('web.Widget'); @@ -6477,7 +6477,7 @@ QUnit.module('Views', { '', intercepts: { execute_action: function (event) { - var effectDescription = pyeval.py_eval(event.data.action_data.effect); + var effectDescription = pyUtils.py_eval(event.data.action_data.effect); assert.deepEqual(effectDescription, {message: 'Congrats!'}, "should have correct effect description"); } } diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index ed40fb36625..ffbbf4ff55e 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -178,7 +178,7 @@ - + @@ -533,7 +533,7 @@ - + From 3248498f4844bbeb06768c064b6fd3a1f400030e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 22 Jun 2018 10:53:56 +0200 Subject: [PATCH 05/11] [IMP] web: implement formatAST utility function In this commit, we implement a function which format a py.js abstract syntax tree into a string. This is really useful whenever we need to manipulate python expressions. --- addons/web/static/src/js/core/py_utils.js | 93 ++++++++++++ .../web/static/tests/core/py_utils_tests.js | 136 +++++++++++++++++- 2 files changed, 224 insertions(+), 5 deletions(-) diff --git a/addons/web/static/src/js/core/py_utils.js b/addons/web/static/src/js/core/py_utils.js index 8590965cee2..c077cfc2e7e 100644 --- a/addons/web/static/src/js/core/py_utils.js +++ b/addons/web/static/src/js/core/py_utils.js @@ -314,6 +314,98 @@ function py_eval(expr, context) { return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null})); } +// 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, +}; + +/** + * 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"); +} return { context: pycontext, @@ -321,6 +413,7 @@ return { eval: pyeval, eval_domains_and_contexts: eval_domains_and_contexts, py_eval: py_eval, + formatAST: formatAST, }; }); diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js index f53c4dca8f6..99fe3c60862 100644 --- a/addons/web/static/tests/core/py_utils_tests.js +++ b/addons/web/static/tests/core/py_utils_tests.js @@ -5,9 +5,21 @@ var Context = require('web.Context'); var pyUtils = require('web.py_utils'); var time = require('web.time'); + +QUnit.assert.checkAST = function (expr, message) { + var ast = py.parse(py.tokenize(expr)); + var formattedAST = pyUtils.formatAST(ast); + this.pushResult({ + result: expr === formattedAST, + actual: formattedAST, + expected: expr, + message: message + }); +}; + QUnit.module('core', function () { - QUnit.module('PY.eval'); + QUnit.module('py_utils'); QUnit.test('simple python expression', function(assert) { assert.expect(2); @@ -419,7 +431,7 @@ QUnit.module('core', function () { assert.strictEqual(result, "2012-02-15 00:00:00"); }); - QUnit.module('pyeval (eval domain contexts)', { + QUnit.module('py_utils (eval domain contexts)', { beforeEach: function() { this.user_context = { uid: 1, @@ -696,7 +708,7 @@ QUnit.module('core', function () { }); }); - QUnit.module('pyeval (contexts)'); + QUnit.module('py_utils (contexts)'); QUnit.test('context_recursive', function (assert) { assert.expect(3); @@ -915,7 +927,7 @@ QUnit.module('core', function () { }); }); - QUnit.module('pyeval (domains)'); + QUnit.module('py_utils (domains)'); QUnit.test('current_date', function (assert) { assert.expect(1); @@ -948,7 +960,7 @@ QUnit.module('core', function () { [['foo', '=', 'qux'], ['bar', '>=', 42]]); }); - QUnit.module('pyeval (groupbys)'); + QUnit.module('py_utils (groupbys)'); QUnit.test('groupbys_00', function (assert) { assert.expect(1); @@ -1033,6 +1045,120 @@ QUnit.module('core', function () { assert.deepEqual(result, ['foo', 'bar', 'grault']); }); + QUnit.module('pyutils (formatAST)'); + + QUnit.test("basic values", function (assert) { + assert.expect(6); + + assert.checkAST("1", "integer value"); + assert.checkAST("1.4", "float value"); + assert.checkAST("-12", "negative integer value"); + assert.checkAST("True", "boolean"); + assert.checkAST("'some string'", "a string"); + assert.checkAST("None", "None"); + }); + + QUnit.test("dictionary", function (assert) { + assert.expect(3); + + assert.checkAST("{}", "empty dictionary"); + assert.checkAST("{'a': 1}", "dictionary with a single key"); + assert.checkAST("d['a']", "get a value in a dictionary"); + }); + + QUnit.test("list", function (assert) { + assert.expect(2); + + assert.checkAST("[]", "empty list"); + assert.checkAST("[1]", "list with one value"); + }); + + QUnit.test("tuple", function (assert) { + assert.expect(2); + + assert.checkAST("()", "empty tuple"); + assert.checkAST("(1, 2)", "basic tuple"); + }); + + QUnit.test("simple arithmetic", function (assert) { + assert.expect(15); + + assert.checkAST("1 + 2", "addition"); + assert.checkAST("+(1 + 2)", "other addition, prefix"); + assert.checkAST("1 - 2", "substraction"); + assert.checkAST("-1 - 2", "other substraction"); + assert.checkAST("-(1 + 2)", "other substraction"); + assert.checkAST("1 + 2 + 3", "addition of 3 integers"); + assert.checkAST("a + b", "addition of two variables"); + assert.checkAST("42 % 5", "modulo operator"); + assert.checkAST("a * 10", "multiplication"); + assert.checkAST("a ** 10", "**"); + assert.checkAST("~10", "bitwise not"); + assert.checkAST("~(10 + 3)", "bitwise not"); + assert.checkAST("a * (1 + 2)", "multiplication and addition"); + assert.checkAST("(a + b) * 43", "addition and multiplication"); + assert.checkAST("a // 10", "integer division"); + }); + + QUnit.test("boolean operators", function (assert) { + assert.expect(6); + + assert.checkAST("True and False", "boolean operator"); + assert.checkAST("True or False", "boolean operator or"); + assert.checkAST("(True or False) and False", "boolean operators and and or"); + assert.checkAST("not False", "not prefix"); + assert.checkAST("not foo", "not prefix with variable"); + assert.checkAST("not a in b", "not prefix with expression"); + }); + + QUnit.test("other operators", function (assert) { + assert.expect(7); + + assert.checkAST("x == y", "== operator"); + assert.checkAST("x != y", "!= operator"); + assert.checkAST("x < y", "< operator"); + assert.checkAST("x is y", "is operator"); + assert.checkAST("x is not y", "is and not operator"); + assert.checkAST("x in y", "in operator"); + assert.checkAST("x not in y", "not in operator"); + }); + + QUnit.test("equality", function (assert) { + assert.expect(1); + assert.checkAST("a == b", "simple equality"); + }); + + QUnit.test("strftime", function (assert) { + assert.expect(3); + assert.checkAST("time.strftime('%Y')", "strftime with year"); + assert.checkAST("time.strftime('%Y') + '-01-30'", "strftime with year"); + assert.checkAST("time.strftime('%Y-%m-%d %H:%M:%S')", "strftime with year"); + }); + + QUnit.test("context_today", function (assert) { + assert.expect(1); + assert.checkAST("context_today().strftime('%Y-%m-%d')", "context today call"); + }); + + + QUnit.test("function call", function (assert) { + assert.expect(5); + assert.checkAST("td()", "simple call"); + assert.checkAST("td(a, b, c)", "simple call with args"); + assert.checkAST('td(days = 1)', "simple call with kwargs"); + assert.checkAST('f(1, 2, days = 1)', "mixing args and kwargs"); + assert.checkAST('str(td(2))', "function call in function call"); + }); + + QUnit.test("various expressions", function (assert) { + assert.expect(3); + assert.checkAST('(a - b).days', "substraction and .days"); + assert.checkAST('a + day == date(2002, 3, 3)'); + + var expr = "[('type', '=', 'in'), ('day', '<=', time.strftime('%Y-%m-%d')), ('day', '>', (context_today() - datetime.timedelta(days = 15)).strftime('%Y-%m-%d'))]"; + assert.checkAST(expr); + }); + }); }); From 0194d85b67c38701befa7d8560a211358fcf79e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 22 Jun 2018 11:47:51 +0200 Subject: [PATCH 06/11] [IMP] web: implement normalize domain utility function In this commit, we implement a function which normalize a domain represented as a py.js syntax tree. --- addons/web/static/src/js/core/py_utils.js | 30 ++++++++++++++ .../web/static/tests/core/py_utils_tests.js | 39 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/addons/web/static/src/js/core/py_utils.js b/addons/web/static/src/js/core/py_utils.js index c077cfc2e7e..f275302279b 100644 --- a/addons/web/static/src/js/core/py_utils.js +++ b/addons/web/static/src/js/core/py_utils.js @@ -407,6 +407,35 @@ function formatAST(ast, lbp) { throw new Error("Unimplemented python construct"); } +/** + * 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 normalizeDomain(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, @@ -414,6 +443,7 @@ return { eval_domains_and_contexts: eval_domains_and_contexts, py_eval: py_eval, formatAST: formatAST, + normalizeDomain: normalizeDomain, }; }); diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js index 99fe3c60862..fafb3d8f6ca 100644 --- a/addons/web/static/tests/core/py_utils_tests.js +++ b/addons/web/static/tests/core/py_utils_tests.js @@ -1159,6 +1159,45 @@ QUnit.module('core', function () { assert.checkAST(expr); }); + QUnit.module('pyutils (normalizeDomain)'); + + QUnit.assert.checkNormalization = function (domainStr, normalizedDomain) { + normalizedDomain = normalizedDomain || domainStr; + var domainAST = py.parse(py.tokenize(domainStr)); + var normalizedDomainAST = pyUtils.normalizeDomain(domainAST); + var result = pyUtils.formatAST(normalizedDomainAST); + this.pushResult({ + result: result === normalizedDomain, + actual: result, + expected: normalizedDomain + }); + }; + + + QUnit.test("return simple (normalized) domains", function (assert) { + assert.expect(3); + + assert.checkNormalization("[]"); + assert.checkNormalization("[('a', '=', 1)]"); + assert.checkNormalization("['!', ('a', '=', 1)]"); + }); + + QUnit.test("properly add the & in a non normalized domain", function (assert) { + assert.expect(1); + assert.checkNormalization( + "[('a', '=', 1), ('b', '=', 2)]", + "['&', ('a', '=', 1), ('b', '=', 2)]" + ); + }); + + QUnit.test("normalize domain with ! operator", function (assert) { + assert.expect(1); + assert.checkNormalization( + "['!', ('a', '=', 1), ('b', '=', 2)]", + "['&', '!', ('a', '=', 1), ('b', '=', 2)]" + ); + }); + }); }); From 5d3f42a9ad3762b4871cbb4687276a530ffff8f7 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Fri, 22 Jun 2018 16:55:40 +0200 Subject: [PATCH 07/11] [IMP] web: implement assemble domain utility function In this commit, we implement a function which allow to combine a list of domains into a single normalized using a 'OR' or an 'AND' operator. We also refactor slightly py_utils. --- addons/web/static/src/js/core/py_utils.js | 101 +++++++++++++++--- .../static/src/js/views/basic/basic_view.js | 2 +- .../web/static/tests/core/py_utils_tests.js | 59 ++++++++-- 3 files changed, 136 insertions(+), 26 deletions(-) diff --git a/addons/web/static/src/js/core/py_utils.js b/addons/web/static/src/js/core/py_utils.js index f275302279b..3ebb79d49ae 100644 --- a/addons/web/static/src/js/core/py_utils.js +++ b/addons/web/static/src/js/core/py_utils.js @@ -314,6 +314,59 @@ 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 = { @@ -323,6 +376,7 @@ var BINDING_POWERS = { }; /** + * @private * Convert a python AST (generated by py.js) to a string form, which should * represent the same AST. * @@ -334,7 +388,7 @@ var BINDING_POWERS = { * binding power of *. * @returns {string} */ -function formatAST(ast, lbp) { +function _formatAST(ast, lbp) { lbp = lbp || 0; switch (ast.id) { // basic values @@ -349,21 +403,21 @@ function formatAST(ast, lbp) { case "[": if (ast.second) { // read a value in a dictionary: d['a'] - return formatAST(ast.first) + '[' + formatAST(ast.second) + ']'; + return _formatAST(ast.first) + '[' + _formatAST(ast.second) + ']'; } else { // list: [1, 2] - var values = ast.first.map(formatAST); + 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 _formatAST(kv[0]) + ': ' + _formatAST(kv[1]); }); return '{' + keyValues.join(', ') + '}'; // relations case "=": - return formatAST(ast.first) + ' ' + ast.id + ' ' + formatAST(ast.second); + return _formatAST(ast.first) + ' ' + ast.id + ' ' + _formatAST(ast.second); // operators case "-": case "+": @@ -376,7 +430,7 @@ function formatAST(ast, lbp) { case "or": if (ast.second) { // infix - var r = formatAST(ast.first, ast.lbp) + ' ' + ast.id + ' ' + formatAST(ast.second, ast.lbp); + var r = _formatAST(ast.first, ast.lbp) + ' ' + ast.id + ' ' + _formatAST(ast.second, ast.lbp); if (ast.lbp < lbp) { r = '(' + r + ')'; } @@ -385,29 +439,42 @@ function formatAST(ast, lbp) { // 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); + return ast.id + _formatAST(ast.first, actualBP); case ".": - return formatAST(ast.first, ast.lbp) + '.' + formatAST(ast.second); + return _formatAST(ast.first, ast.lbp) + '.' + _formatAST(ast.second); case "not": - return "not " + formatAST(ast.first); + return "not " + _formatAST(ast.first); case "(comparator)": - var operator = ast.operators[0] - return formatAST(ast.expressions[0]) + ' ' + operator + ' ' + formatAST(ast.expressions[1]); + 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(', ') + ')'; + return _formatAST(ast.first) + '(' + ast.second.map(_formatAST).join(', ') + ')'; } else { // this is a tuple - return '(' + ast.first.map(formatAST).join(', ') + ')'; + 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 @@ -417,7 +484,7 @@ function formatAST(ast, lbp) { * @param {PyJS AST} domain valid AST representing a domain * @returns {PyJS AST} normalized domain AST */ -function normalizeDomain(domain) { +function _normalizeDomainAST(domain) { var expected = 1; for (var i = 0; i < domain.first.length; i++) { var value = domain.first[i].value; @@ -442,8 +509,10 @@ return { eval: pyeval, eval_domains_and_contexts: eval_domains_and_contexts, py_eval: py_eval, - formatAST: formatAST, normalizeDomain: normalizeDomain, + assembleDomains: assembleDomains, + _getPyJSAST: _getPyJSAST, + _formatAST: _formatAST, + _normalizeDomainAST: _normalizeDomainAST, }; - }); 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 cb4afe73476..7bc5e95bfc8 100644 --- a/addons/web/static/src/js/views/basic/basic_view.js +++ b/addons/web/static/src/js/views/basic/basic_view.js @@ -216,7 +216,7 @@ 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), }); } }); diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js index fafb3d8f6ca..1e1848eb778 100644 --- a/addons/web/static/tests/core/py_utils_tests.js +++ b/addons/web/static/tests/core/py_utils_tests.js @@ -7,8 +7,8 @@ var time = require('web.time'); QUnit.assert.checkAST = function (expr, message) { - var ast = py.parse(py.tokenize(expr)); - var formattedAST = pyUtils.formatAST(ast); + var ast = pyUtils._getPyJSAST(expr); + var formattedAST = pyUtils._formatAST(ast); this.pushResult({ result: expr === formattedAST, actual: formattedAST, @@ -1045,7 +1045,7 @@ QUnit.module('core', function () { assert.deepEqual(result, ['foo', 'bar', 'grault']); }); - QUnit.module('pyutils (formatAST)'); + QUnit.module('pyutils (_formatAST)'); QUnit.test("basic values", function (assert) { assert.expect(6); @@ -1159,13 +1159,11 @@ QUnit.module('core', function () { assert.checkAST(expr); }); - QUnit.module('pyutils (normalizeDomain)'); + QUnit.module('pyutils (_normalizeDomain)'); - QUnit.assert.checkNormalization = function (domainStr, normalizedDomain) { - normalizedDomain = normalizedDomain || domainStr; - var domainAST = py.parse(py.tokenize(domainStr)); - var normalizedDomainAST = pyUtils.normalizeDomain(domainAST); - var result = pyUtils.formatAST(normalizedDomainAST); + QUnit.assert.checkNormalization = function (domain, normalizedDomain) { + normalizedDomain = normalizedDomain || domain; + var result = pyUtils.normalizeDomain(domain); this.pushResult({ result: result === normalizedDomain, actual: result, @@ -1198,6 +1196,49 @@ QUnit.module('core', function () { ); }); + QUnit.module('pyutils (assembleDomains)'); + + QUnit.assert.checkAssemble = function (domains, operator, domain) { + domain = pyUtils.normalizeDomain(domain); + var result = pyUtils.assembleDomains(domains, operator); + this.pushResult({ + result: result === domain, + actual: result, + expected: domain + }); + }; + + QUnit.test("assemble domains", function (assert) { + assert.expect(6); + + assert.checkAssemble([], '&', "[]"); + assert.checkAssemble( + ["[('a', '=', '1'), ('b', '!=', 2)]"], + 'AND', + "['&',('a', '=', '1'), ('b', '!=', 2)]" + ); + assert.checkAssemble( + ["[('a', '=', '1')]", "[]"], + 'AND', + "[('a', '=', '1')]" + ); + assert.checkAssemble( + ["[('a', '=', '1')]", "[('b', '<=', 3)]"], + 'AND', + "['&',('a', '=', '1'),('b','<=', 3)]" + ); + assert.checkAssemble( + ["[('a', '=', '1'), ('c', 'in', [4, 5])]", "[('b', '<=', 3)]"], + 'OR', + "['|', '&',('a', '=', '1'),('c', 'in', [4, 5]),('b','<=', 3)]" + ); + assert.checkAssemble( + ["[('user_id', '=', uid)]"], + null, + "[('user_id', '=', uid)]" + ); + }); + }); }); From 9756109e670cef01b4d475c8cb6687fecdab2cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 28 Jun 2018 15:10:11 +0200 Subject: [PATCH 08/11] [IMP] web: add tz_offset function in python eval context --- addons/web/static/src/js/core/py_utils.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/addons/web/static/src/js/core/py_utils.js b/addons/web/static/src/js/core/py_utils.js index 3ebb79d49ae..83e24b207c6 100644 --- a/addons/web/static/src/js/core/py_utils.js +++ b/addons/web/static/src/js/core/py_utils.js @@ -231,6 +231,23 @@ function context_today() { 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 { @@ -240,6 +257,7 @@ function pycontext() { relativedelta: py.extras.relativedelta, current_date: py.PY_call( py.extras.time.strftime, [py.str.fromJSON('%Y-%m-%d')]), + tz_offset: tz_offset, }; } From 097464de6f85020ad1e56bbc253837a7d60e47e4 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Tue, 12 Jun 2018 17:11:13 +0200 Subject: [PATCH 09/11] [IMP] web: easy selection of a period in filters menu 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: 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. --- addons/crm/views/crm_lead_views.xml | 4 +- addons/web/static/lib/py.js/lib/py_extras.js | 9 + addons/web/static/src/js/core/domain.js | 80 +++++++ .../src/js/views/search/favorites_menu.js | 21 +- .../src/js/views/search/filters_menu.js | 86 ++++++-- .../src/js/views/search/groupby_menu.js | 24 +-- .../src/js/views/search/search_inputs.js | 53 +++-- .../static/src/js/views/search/search_view.js | 172 ++++++++++----- .../static/src/js/widgets/dropdown_menu.js | 70 +++--- addons/web/static/src/scss/dropdown_menu.scss | 8 + addons/web/static/src/scss/search_view.scss | 4 + addons/web/static/src/xml/base.xml | 9 +- .../tests/chrome/action_manager_tests.js | 2 +- .../web/static/tests/core/py_utils_tests.js | 9 +- .../tests/views/search_filters_menu_tests.js | 40 ++++ .../tests/views/search_groupby_menu_tests.js | 30 ++- .../static/tests/views/search_view_tests.js | 199 +++++++++++++++--- .../static/tests/views/view_dialogs_tests.js | 2 +- .../tests/widgets/dropdown_menu_tests.js | 42 ++-- odoo/addons/base/rng/common.rng | 2 + 20 files changed, 664 insertions(+), 202 deletions(-) diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 8b38ee39cf6..eed0b44ce05 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -667,6 +667,7 @@ ]"/> + @@ -886,6 +887,7 @@ for rec in records: 'default_type': 'opportunity', 'default_user_id': uid, 'search_default_assigned_to_me': 1, + 'search_default_creation_date': 1, } { 'default_type': 'opportunity', 'default_user_id': uid, - 'search_default_activities_my': 1 + 'search_default_activities_my': 1, } diff --git a/addons/web/static/lib/py.js/lib/py_extras.js b/addons/web/static/lib/py.js/lib/py_extras.js index 2bc69ece598..47aaa33685e 100644 --- a/addons/web/static/lib/py.js/lib/py_extras.js +++ b/addons/web/static/lib/py.js/lib/py_extras.js @@ -444,6 +444,14 @@ datetime.datetime = py.type('datetime', null, { throw new Error('ValueError: No known conversion for ' + m); })); }, + to_utc: function () { + var d = new Date(this.year, this.month, this.day, this.hour, this.minute, this.second); + var offset = d.getTimezoneOffset(); + var kwargs = {minutes: py.float.fromJSON(offset)}; + var timedelta = py.PY_call(py.extras.datetime.timedelta,[],kwargs); + var s = tmxxx(this.year, this.month, this.day + timedelta.days, this.hour, this.minute, this.second + timedelta.seconds); + return datetime.datetime.fromJSON(s.year, s.month, s.day, s.hour, s.minute, s.second); + }, now: py.classmethod.fromJSON(function () { var d = new Date(); return py.PY_call(datetime.datetime, [ @@ -462,6 +470,7 @@ datetime.datetime = py.type('datetime', null, { 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, [ diff --git a/addons/web/static/src/js/core/domain.js b/addons/web/static/src/js/core/domain.js index 86e1f85a634..e8f089dca76 100644 --- a/addons/web/static/src/js/core/domain.js +++ b/addons/web/static/src/js/core/domain.js @@ -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 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 bd622797c49..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,6 +4,7 @@ odoo.define('web.FavoriteMenu', function (require) { var config = require('web.config'); var core = require('web.core'); var data_manager = require('web.data_manager'); +var Domain = require('web.Domain'); var pyUtils = require('web.py_utils'); var session = require('web.session'); var Widget = require('web.Widget'); @@ -98,7 +99,9 @@ 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) { @@ -106,7 +109,7 @@ return Widget.extend({ }, }); var results = pyUtils.eval_domains_and_contexts({ - domains: search.domains, + 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,12 +142,58 @@ 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 * 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 d3f36254819..ee179d7da74 100644 --- a/addons/web/static/src/js/views/search/search_inputs.js +++ b/addons/web/static/src/js/views/search/search_inputs.js @@ -518,7 +518,7 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ * 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) && @@ -532,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 () { @@ -640,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(); @@ -705,13 +722,21 @@ var FilterGroup = Input.extend(/** @lends instance.web.search.FilterGroup# */{ })); }, /* - * private - * * @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 e5a00434745..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,6 +4,7 @@ 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'); @@ -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; @@ -193,11 +197,15 @@ var FacetView = Widget.extend({ * @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) { + 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 () { @@ -212,9 +220,21 @@ var FacetView = Widget.extend({ if (index > 0) { $('', {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); })); }); }, @@ -232,21 +252,34 @@ var FacetValueView = Widget.extend({ * corresponds to a groupby with an associated 'date' * 'datetime' field. */ - init: function (parent, model, interval) { + 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); }, @@ -292,7 +325,7 @@ var SearchView = Widget.extend({ menu_item_toggled: '_onItemToggled', item_option_changed: '_onItemOptionChanged', new_groupby: '_onNewGroupby', - new_filter: '_onNewFilter', + new_filters: '_onNewFilters', }, defaults: _.extend({}, Widget.prototype.defaults, { hidden: false, @@ -326,6 +359,8 @@ var SearchView = Widget.extend({ this.action = this.options.action || {}; this.search_fields = []; + this.hasFavorites = false; + this.selectedGroupIds = { groupByCategory: [], filterCategory: [] @@ -334,6 +369,7 @@ var SearchView = Widget.extend({ this.groupbysMapping = []; this.filtersMapping = []; this.intervalMapping = []; + this.periodMapping = []; this.filters = []; this.groupbys = []; @@ -406,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}); @@ -438,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: @@ -451,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); } @@ -470,7 +512,6 @@ var SearchView = Widget.extend({ } }); var intervalMappingNormalized = {}; - _.each(this.intervalMapping, function (couple) { var fieldName = couple.groupby.fieldName; var interval = couple.interval; @@ -589,7 +630,6 @@ var SearchView = Widget.extend({ groupByCategory: [], filterCategory: [], }; - this.query.each(function (facet) { if (facet.attributes.cat === "groupByCategory") { @@ -623,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); @@ -687,10 +727,16 @@ var SearchView = Widget.extend({ 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, @@ -705,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 = []; } @@ -718,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)); @@ -752,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}); @@ -804,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'}); @@ -835,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 (_.contains(['date', 'datetime'], self.fields[fieldName].type)) { - 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}); } @@ -882,10 +942,12 @@ var SearchView = Widget.extend({ * @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; }, @@ -902,10 +964,12 @@ var SearchView = Widget.extend({ * @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; }, @@ -944,6 +1008,11 @@ 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); } }, @@ -956,18 +1025,19 @@ var SearchView = Widget.extend({ * @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, + 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, @@ -978,6 +1048,7 @@ 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 @@ -987,7 +1058,7 @@ var SearchView = Widget.extend({ * @private * @param {OdooEvent} event */ - _onNewFilter: function (event) { + _onNewFilters: function (event) { var self= this; var filter; var filters = []; @@ -1006,16 +1077,16 @@ var SearchView = Widget.extend({ groupId = filterDescription.groupId; } }); - var group = new search_inputs.FilterGroup(filters, this, this.intervalMapping); + 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 @@ -1036,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/widgets/dropdown_menu.js b/addons/web/static/src/js/widgets/dropdown_menu.js index 2999d09bbf9..335eed80e0e 100644 --- a/addons/web/static/src/js/widgets/dropdown_menu.js +++ b/addons/web/static/src/js/widgets/dropdown_menu.js @@ -13,7 +13,6 @@ var DropdownMenu = Widget.extend({ 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', }, @@ -30,7 +29,7 @@ var DropdownMenu = Widget.extend({ * - 'style' (button style) * @param {Object} items list of menu items (type IGMenuItem below) * interface IMenuItem { - * itemId: string; unique id associated with the item + * 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 @@ -38,7 +37,6 @@ var DropdownMenu = Widget.extend({ * 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) * } */ @@ -57,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'); }, @@ -117,13 +117,14 @@ var DropdownMenu = Widget.extend({ * @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 @@ -131,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) { - if (item.isActive) { - item.currentOptionId = item.defaultOptionId; - eventData.optionId = item.currentOptionId; - } else { - item.currentOptionId = false; - } + 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) { + item.isOpen = !item.isOpen; + this._renderMenuItems(); } - this._renderMenuItems(); - this.trigger_up('menu_item_toggled', eventData); }, /** * @private @@ -178,15 +177,6 @@ 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 @@ -235,18 +225,6 @@ 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; diff --git a/addons/web/static/src/scss/dropdown_menu.scss b/addons/web/static/src/scss/dropdown_menu.scss index c32779271bc..8024783d3c2 100644 --- a/addons/web/static/src/scss/dropdown_menu.scss +++ b/addons/web/static/src/scss/dropdown_menu.scss @@ -43,8 +43,16 @@ } } } + li.divider:first-child { + display: none; + } .o_item_option { + color: $o-main-text-color; padding-left: 20px; + &:hover { + color: darken($o-main-text-color, 10%); + background-color: $dropdown-link-hover-bg; + } } } } diff --git a/addons/web/static/src/scss/search_view.scss b/addons/web/static/src/scss/search_view.scss index 03fe29916fc..0ff5fd320e7 100644 --- a/addons/web/static/src/scss/search_view.scss +++ b/addons/web/static/src/scss/search_view.scss @@ -179,6 +179,10 @@ display: none; // Needed by the webclient @include o-search-options-dropdown-custom-li; } + .o_submenu_switcher { + position: absolute; + right: 0px; + } } diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index a4714e81ee5..758f5bcd416 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1055,14 +1055,19 @@ - + + +
    + +
  • +
@@ -1145,7 +1150,7 @@ - + diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 3b8f320393b..3910e6d761f 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -2877,7 +2877,7 @@ QUnit.module('ActionManager', { intercepts: { create_filter: function (event) { var filter = event.data.filter; - assert.deepEqual(filter.domain, [['bar', '=', 1]], + assert.deepEqual(filter.domain, "[('bar', '=', 1)]", "should save the correct domain"); assert.deepEqual(filter.context, {shouldBeInFilterContext: true}, "should save the correct context"); diff --git a/addons/web/static/tests/core/py_utils_tests.js b/addons/web/static/tests/core/py_utils_tests.js index 1e1848eb778..df657789c71 100644 --- a/addons/web/static/tests/core/py_utils_tests.js +++ b/addons/web/static/tests/core/py_utils_tests.js @@ -33,9 +33,9 @@ QUnit.module('core', function () { QUnit.test('simple arithmetic', function(assert) { assert.expect(2); - var result = pyEval.py_eval("1 + 2"); + var result = pyUtils.py_eval("1 + 2"); assert.strictEqual(result, 3, "should properly evaluate sum"); - result = pyEval.py_eval("42 % 5"); + result = pyUtils.py_eval("42 % 5"); assert.strictEqual(result, 2, "should properly evaluate modulo operator"); }); @@ -1209,9 +1209,10 @@ QUnit.module('core', function () { }; QUnit.test("assemble domains", function (assert) { - assert.expect(6); + assert.expect(7); assert.checkAssemble([], '&', "[]"); + assert.checkAssemble(["[('a', '=', 1)]"], null, "[('a', '=', 1)]"); assert.checkAssemble( ["[('a', '=', '1'), ('b', '!=', 2)]"], 'AND', @@ -1238,7 +1239,5 @@ QUnit.module('core', function () { "[('user_id', '=', uid)]" ); }); - }); - }); diff --git a/addons/web/static/tests/views/search_filters_menu_tests.js b/addons/web/static/tests/views/search_filters_menu_tests.js index 84bc1df86d8..e6430527de7 100644 --- a/addons/web/static/tests/views/search_filters_menu_tests.js +++ b/addons/web/static/tests/views/search_filters_menu_tests.js @@ -129,7 +129,47 @@ QUnit.module('FiltersMenu', { filterMenu.destroy(); }); + QUnit.test('adding a date filter', function (assert) { + assert.expect(5); + var filters = [{ + itemId: 1, + isActive: false, + description: 'some filter', + isPeriod: true, + name: 'Filter Date', + fieldName: 'date_field', + groupId: 1, + }, + ]; + var filterMenu = createFiltersMenu(filters, this.fields, { + intercepts: { + menu_item_toggled: function (ev) { + assert.strictEqual(ev.data.optionId, 'this_quarter', + "should trigger event with proper period"); + }, + }, + }); + // open menu dropdown + filterMenu.$('span.fa-filter').click(); + + assert.strictEqual(filterMenu.$('.o_menu_item ul').length, 0, + "there should not be a sub menu item list"); + + // open date sub menu + filterMenu.$('.o_submenu_switcher').click(); + assert.strictEqual(filterMenu.$('.o_menu_item ul').length, 1, + "there should be a sub menu item list"); + + // click on This Quarter option + assert.notOk(filterMenu.$('.o_menu_item li[data-option_id="this_quarter"]').hasClass('selected'), + "menu item should not be selected"); + filterMenu.$('.o_menu_item li[data-option_id="this_quarter"] a').click(); + assert.ok(filterMenu.$('.o_menu_item li[data-option_id="this_quarter"]').hasClass('selected'), + "menu item should be selected"); + + filterMenu.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/search_groupby_menu_tests.js b/addons/web/static/tests/views/search_groupby_menu_tests.js index e403c08a97c..8dff7a581fe 100644 --- a/addons/web/static/tests/views/search_groupby_menu_tests.js +++ b/addons/web/static/tests/views/search_groupby_menu_tests.js @@ -93,14 +93,22 @@ QUnit.module('GroupByMenu', { groupByMenu.destroy(); }); - QUnit.test('click on groupby filter should activate it', function (assert) { + QUnit.test('click on a groupby filter (not of date type) should activate it', function (assert) { assert.expect(5); - var groupByMenu = createGroupByMenu(this.groupbys, - {fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}}, { + this.groupbys = [{ + isActive: false, + description: 'another group by', + fieldName: 'float_field', + itemId: 'green', + groupId: 1, + }]; + this.fields = {float_field: {sortable: true, string: 'Super Float', type: 'float'}}; + + var groupByMenu = createGroupByMenu(this.groupbys, this.fields, { intercepts: { menu_item_toggled: function(ev) { - assert.strictEqual(ev.data.itemId, 'red'); + assert.strictEqual(ev.data.itemId, 'green'); assert.strictEqual(ev.data.isActive, true); }, }, @@ -111,7 +119,21 @@ QUnit.module('GroupByMenu', { assert.ok(groupByMenu.$('.o_menu_item:first').hasClass('selected')); assert.ok(groupByMenu.$('.o_menu_item:first').is(':visible'), 'group by filter should still be visible'); + groupByMenu.destroy(); + }); + QUnit.test('click on a groupby filter of date type should open menu option', function (assert) { + assert.expect(4); + + var groupByMenu = createGroupByMenu(this.groupbys, + {fieldname: {sortable: true, string: 'Super Date', type: 'date', isDate: true}}); + groupByMenu.$('button:first').click(); + assert.ok(!groupByMenu.$('.o_menu_item:first').hasClass('selected')); + groupByMenu.$('.o_menu_item a').first().click(); + assert.ok(!groupByMenu.$('.o_menu_item:first').hasClass('selected')); + assert.ok(groupByMenu.$('.o_menu_item:first').is(':visible'), + 'group by filter should still be visible'); + assert.ok(groupByMenu.$('.o_item_option').length, 5); groupByMenu.destroy(); }); diff --git a/addons/web/static/tests/views/search_view_tests.js b/addons/web/static/tests/views/search_view_tests.js index e4db69131f7..f01ea2f05af 100644 --- a/addons/web/static/tests/views/search_view_tests.js +++ b/addons/web/static/tests/views/search_view_tests.js @@ -10,17 +10,17 @@ QUnit.module('Search View', { partner: { fields: { date_field: {string: "Date", type: "date", store: true, sortable: true}, - Birthday: {string: "Birthday", type: "date", store: true, sortable: true}, - foo: {string: "Foo", type: "char"}, + birthday: {string: "Birthday", type: "date", store: true, sortable: true}, + foo: {string: "Foo", type: "char", store: true, sortable: true}, bar: {string: "Bar", type: "many2one", relation: 'partner'}, float_field: {string: "Float", type: "float"}, }, records: [ - {id: 1, display_name: "First record", foo: "yop", bar: 2, date_field: "2017-01-25", Birthday: "1983-07-15", float_field: 1}, - {id: 2, display_name: "Second record", foo: "blip", bar: 1, date_field: "2017-01-24", Birthday: "1982-06-04",float_field: 2}, - {id: 3, display_name: "Third record", foo: "gnap", bar: 1, date_field: "2017-01-13", Birthday: "1985-09-13",float_field: 1.618}, - {id: 4, display_name: "Fourth record", foo: "plop", bar: 2, date_field: "2017-02-25", Birthday: "1983-05-05",float_field: -1}, - {id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, date_field: "2016-01-25", Birthday: "1800-01-01",float_field: 13}, + {id: 1, display_name: "First record", foo: "yop", bar: 2, date_field: "2017-01-25", birthday: "1983-07-15", float_field: 1}, + {id: 2, display_name: "Second record", foo: "blip", bar: 1, date_field: "2017-01-24", birthday: "1982-06-04",float_field: 2}, + {id: 3, display_name: "Third record", foo: "gnap", bar: 1, date_field: "2017-01-13", birthday: "1985-09-13",float_field: 1.618}, + {id: 4, display_name: "Fourth record", foo: "plop", bar: 2, date_field: "2017-02-25", birthday: "1983-05-05",float_field: -1}, + {id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, date_field: "2016-01-25", birthday: "1800-01-01",float_field: 13}, ], }, pony: { @@ -69,6 +69,13 @@ QUnit.module('Search View', { type: 'ir.actions.act_window', views: [[2, 'list']], search_view_id: [4, 'search'], + }, { + id: 6, + name: 'Partners Action 6', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[2, 'list']], + search_view_id: [5, 'search'], } ]; @@ -90,21 +97,21 @@ QUnit.module('Search View', { // search views 'partner,false,search': ''+ - '' + '' + '', 'partner,2,search': ''+ - '' + '' + '', 'partner,3,search': ''+ - '' + '' + '', 'partner,4,search': ''+ - '' + '' + '', + 'partner,5,search': ''+ + '' + + '' + + '', }; }, }, function () { @@ -159,13 +166,14 @@ QUnit.module('Search View', { actionManager.doAction(2); $('span.fa-bars').click(); $('.o_submenu_switcher').click(); - $('.o_item_option:first').click(); - assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 1, - 'should have a facet'); + // Don't forget there is a hidden li.divider element at first place among children $('.o_item_option:nth-child(2)').click(); assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 1, 'should have a facet'); - $('.o_item_option:nth-child(2)').click(); + $('.o_item_option:nth-child(3)').click(); + assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 1, + 'should have a facet'); + $('.o_item_option:nth-child(3)').click(); assert.strictEqual($('.o_searchview .o_searchview_facet .o_facet_values span').length, 0, 'should have no facet'); actionManager.destroy(); @@ -213,12 +221,26 @@ QUnit.module('Search View', { }); QUnit.test('group by a date field using interval works', function (assert) { - assert.expect(9); + assert.expect(13); + + + var groupbys = [ + ["date_field:day"], + ["date_field:month"], + ["birthday:month"], + ["birthday:year"], + ]; var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/partner/read_group') { + assert.deepEqual(args.kwargs.groupby, groupbys.shift()); + } + return this._super.apply(this, arguments); + }, }); actionManager.doAction(5); @@ -226,32 +248,31 @@ QUnit.module('Search View', { $('.o_search_options .fa-bars').click(); // Activate the groupby 'Date' $('.o_group_by_menu .o_menu_item').click(); - // by default, data should be grouped by the field 'Date' using the interval 'day' + // // select option 'day' + $('.o_group_by_menu .o_menu_item .o_item_option[data-option_id="day"]').click(); assert.strictEqual($('div.o_facet_values span').text().trim(),'Date: Day'); assert.strictEqual($('.o_content tr.o_group_header').length, 5); - // open submenu with interval options - $('.o_group_by_menu .o_menu_item .o_submenu_switcher').click(); - // select option 'month' - $('.o_group_by_menu .o_menu_item .o_item_option:nth-child(3)').click(); - // data should be grouped by the field 'Date' using the interval 'month' + // // select option 'month' + $('.o_group_by_menu .o_menu_item .o_item_option[data-option_id="month"]').click(); + // // data should be grouped by the field 'Date' using the interval 'month' assert.strictEqual($('div.o_facet_values span').text().trim(),'Date: Month'); assert.strictEqual($('.o_content tr.o_group_header').length, 3); - // deactivate option 'month' - $('.o_group_by_menu .o_menu_item .o_item_option:nth-child(3)').click(); - // no groupby is applied + // // deactivate option 'month' + $('.o_group_by_menu .o_menu_item .o_item_option[data-option_id="month"]').click(); + // // no groupby is applied assert.strictEqual($('div.o_facet_values span').length, 0); - // open 'Add custom Groupby' menu + // // open 'Add custom Groupby' menu $('.o_group_by_menu .o_add_custom_group a').click(); - // click on 'Apply' button + // // click on 'Apply' button $('.o_group_by_menu .o_generator_menu button').click(); - // data should be grouped by the field 'Birthday' using the interval 'month' + // // data should be grouped by the field 'Birthday' using the interval 'month' assert.strictEqual($('div.o_facet_values span').text().trim(),'Birthday: Month'); assert.strictEqual($('.o_content tr.o_group_header').length, 5); - // open submenu with interval options + // // open submenu with interval options $('.o_group_by_menu .o_menu_item .o_submenu_switcher').eq(1).click(); - // select option 'year' + // // select option 'year' $('.o_group_by_menu .o_menu_item .o_item_option').eq(9).click(); - // data should be grouped by the field 'Birthday' using the interval 'year' + // // data should be grouped by the field 'Birthday' using the interval 'year' assert.strictEqual($('div.o_facet_values span').text().trim(),'Birthday: Year'); assert.strictEqual($('.o_content tr.o_group_header').length, 4); actionManager.destroy(); @@ -295,5 +316,121 @@ QUnit.module('Search View', { 'no facet should be in the search view'); actionManager.destroy(); }); + + QUnit.test('filter by a date field using period works', function (assert) { + + assert.expect(14); + + this.archs['partner,4,search'] = ''+ + '' + + ''; + + var domains = [ + [], + // today + ['&', ["date_field", ">=", "2017-03-22"],["date_field", "<", "2017-03-23"]], + // this week + ['&', ["date_field", ">=", "2017-03-20"],["date_field", "<", "2017-03-27"]], + // this month + ['&', ["date_field", ">=", "2017-03-01"],["date_field", "<", "2017-04-01"]], + // this quarter + ['&', ["date_field", ">=", "2017-01-01"],["date_field", "<", "2017-04-01"]], + // this year + ['&', ["date_field", ">=", "2017-01-01"],["date_field", "<", "2018-01-01"]], + // yesterday + ['&', ["date_field", ">=", "2017-03-21"],["date_field", "<", "2017-03-22"]], + // last week + ['&', ["date_field", ">=", "2017-03-13"],["date_field", "<", "2017-03-20"]], + // last month + ['&', ["date_field", ">=", "2017-02-01"],["date_field", "<", "2017-03-01"]], + // last quarter + ['&', ["date_field", ">=", "2016-10-01"],["date_field", "<", "2017-01-01"]], + // last year + ['&', ["date_field", ">=", "2016-01-01"],["date_field", "<", "2017-01-01"]], + // last 7 days (whole days) + ['&', ["date_field", ">=", "2017-03-15"],["date_field", "<", "2017-03-22"]], + // last 30 days + ['&', ["date_field", ">=", "2017-02-20"],["date_field", "<", "2017-03-22"]], + // last 365 days + ['&', ["date_field", ">=", "2016-03-22"],["date_field", "<", "2017-03-22"]], + ]; + + var RealDate = window.Date; + + window.Date = function TestDate() { + // month are indexed from 0! + return new RealDate(2017,2,22); + }; + window.Date.now = function Test() { + return new Date(2017,2,22); + }; + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + if (route === '/web/dataset/search_read') { + assert.deepEqual(args.domain, domains.shift()); + } + return this._super.apply(this, arguments); + }, + }); + + actionManager.doAction(5); + + // open menu 'Filter' + $('.o_search_options .fa-filter').click(); + // open menu options + $('li.o_menu_item').click(); + $('li.o_menu_item .o_item_option[data-option_id="today"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="this_week"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="this_month"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="this_quarter"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="this_year"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="yesterday"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_week"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_month"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_quarter"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_year"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_7_days"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_30_days"]').click(); + $('li.o_menu_item .o_item_option[data-option_id="last_365_days"]').click(); + + actionManager.destroy(); + window.Date = RealDate; + }); + + QUnit.module('Favorites Menu'); + + QUnit.test('dynamic filters are saved dynamic', function (assert) { + assert.expect(1); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + create_filter: function (ev) { + console.log(ev.data); + assert.equal( + ev.data.filter.domain, + "['&', " + + "('date_field', '>=', (context_today() + relativedelta()).strftime('%Y-%m-%d')), " + + "('date_field', '<', (context_today() + relativedelta(days = 1)).strftime('%Y-%m-%d'))"+ + "]"); + }, + }, + }); + + actionManager.doAction(6); + $('span.fa-filter').click(); + $('.o_filters_menu li.o_menu_item a').click(); + $('.o_item_option:first').click(); + $('span.fa-star').click(); + $('.o_favorites_menu .o_save_search a').click(); + $('.o_favorites_menu .o_save_name button').click(); + actionManager.destroy(); + }); }); }); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 2d6af1b5e14..14ec4402061 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -391,7 +391,7 @@ QUnit.module('Views', { intercepts: { create_filter: function (event) { var filter = event.data.filter; - assert.deepEqual(filter.domain, [['bar', '=', true]], + assert.deepEqual(filter.domain, "[('bar', '=', True)]", "should save the correct domain"); assert.deepEqual(filter.context, {shouldBeInFilterContext: true}, "should save the correct context"); diff --git a/addons/web/static/tests/widgets/dropdown_menu_tests.js b/addons/web/static/tests/widgets/dropdown_menu_tests.js index 92444c57075..0669b92c87f 100644 --- a/addons/web/static/tests/widgets/dropdown_menu_tests.js +++ b/addons/web/static/tests/widgets/dropdown_menu_tests.js @@ -98,14 +98,16 @@ QUnit.module('Web', { QUnit.test('options rendering', function (assert) { assert.expect(3); - this.items[0].options = [{optionId: 1, description: "First Option"}, {optionId: 2, description: "Second Option"}]; + this.items[0].options = [{optionId: 1, description: "First Option", groupId: 1}, {optionId: 2, description: "Second Option", groupId: 1}]; var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items); // open dropdown dropdownMenu.$('button:first').click(); assert.strictEqual(dropdownMenu.$('li').length, 4); - dropdownMenu.$('span.fa-caret-down').click(); - assert.strictEqual(dropdownMenu.$('li').length, 6); + // open options menu + dropdownMenu.$('span.fa-caret-right').click(); + assert.strictEqual(dropdownMenu.$('li').length, 7); + // close options menu dropdownMenu.$('span.fa-caret-down').click(); assert.strictEqual(dropdownMenu.$('li').length, 4); @@ -119,8 +121,9 @@ QUnit.module('Web', { var dropdownMenu = createDropdownMenu(this.dropdownHeader, this.items); dropdownMenu.$('button:first').click(); - dropdownMenu.$('span.fa-caret-down').click(); - assert.strictEqual(dropdownMenu.$('li').length, 6); + // open options menu + dropdownMenu.$('span.fa-caret-right').click(); + assert.strictEqual(dropdownMenu.$('li').length, 7); dropdownMenu.$('button:first').click(); dropdownMenu.$('button:first').click(); assert.strictEqual(dropdownMenu.$('li').length, 4); @@ -158,24 +161,27 @@ QUnit.module('Web', { }, }, }); + // open dropdown menu dropdownMenu.$('button:first').click(); - dropdownMenu.$('span.fa-caret-down').click(); - assert.strictEqual(dropdownMenu.$('li').length, 6); - assert.ok(!dropdownMenu.$('.o_menu_item:first').hasClass('selected')); - assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected')); + // open options menu + dropdownMenu.$('span.fa-caret-right').click(); + assert.strictEqual(dropdownMenu.$('li').length, 7); + // Don't forget there is a hidden li.divider element at first place among children + assert.ok(!dropdownMenu.$('.o_menu_item:nth-child(2)').hasClass('selected')); assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); + assert.ok(!dropdownMenu.$('.o_item_option:nth-child(3)').hasClass('selected')); dropdownMenu.$('li.o_item_option:first').click(); - assert.ok(dropdownMenu.$('.o_menu_item:first').hasClass('selected')); - assert.ok(dropdownMenu.$('.o_item_option:first').hasClass('selected')); - assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); - dropdownMenu.$('li.o_item_option:nth-child(2)').click(); - assert.ok(dropdownMenu.$('.o_menu_item:first').hasClass('selected')); - assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected')); + assert.ok(dropdownMenu.$('.o_menu_item:nth-child(2)').hasClass('selected')); assert.ok(dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); - dropdownMenu.$('li.o_item_option:nth-child(2)').click(); - assert.ok(!dropdownMenu.$('.o_menu_item:first').hasClass('selected')); - assert.ok(!dropdownMenu.$('.o_item_option:first').hasClass('selected')); + assert.ok(!dropdownMenu.$('.o_item_option:nth-child(3)').hasClass('selected')); + dropdownMenu.$('li.o_item_option:nth-child(3)').click(); + assert.ok(dropdownMenu.$('.o_menu_item:nth-child(2)').hasClass('selected')); assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); + assert.ok(dropdownMenu.$('.o_item_option:nth-child(3)').hasClass('selected')); + dropdownMenu.$('li.o_item_option:nth-child(3)').click(); + assert.ok(!dropdownMenu.$('.o_menu_item:nth-child(2)').hasClass('selected')); + assert.ok(!dropdownMenu.$('.o_item_option:nth-child(2)').hasClass('selected')); + assert.ok(!dropdownMenu.$('.o_item_option:nth-child(3)').hasClass('selected')); dropdownMenu.destroy(); }); diff --git a/odoo/addons/base/rng/common.rng b/odoo/addons/base/rng/common.rng index cd6bb78a073..3e919201786 100644 --- a/odoo/addons/base/rng/common.rng +++ b/odoo/addons/base/rng/common.rng @@ -372,6 +372,8 @@ + + From f9874b2b047332bba5a032a506f63e8d453794bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 15 Jun 2018 10:36:31 +0200 Subject: [PATCH 10/11] [IMP] *: adaptation of search views It is now possible to easily filter data using periods of time (e.g. Creation Date: This Week). This adds greater flexibility to the search view and has made some old filters become useless. In this commit, we alter the search views in order to profit of the new framework possibilities. --- .../report/account_invoice_report_view.xml | 4 +--- .../crm/report/crm_activity_report_views.xml | 3 +-- .../report/crm_opportunity_report_views.xml | 8 +++++--- addons/crm/views/crm_lead_views.xml | 10 ++++------ .../views/hr_attendance_view.xml | 4 +--- .../views/hr_recruitment_views.xml | 2 +- .../hr_timesheet/views/hr_timesheet_views.xml | 13 +------------ .../hr_timesheet_attendance_report_view.xml | 13 +------------ .../im_livechat_report_channel_views.xml | 2 +- .../im_livechat_report_operator_views.xml | 2 +- addons/im_livechat/views/rating_views.xml | 2 +- addons/lunch/views/lunch_views.xml | 2 +- addons/mrp/views/mrp_workcenter_views.xml | 4 +--- addons/mrp/views/mrp_workorder_views.xml | 6 ++---- .../views/pos_order_report_view.xml | 2 +- .../point_of_sale/views/pos_session_view.xml | 2 +- addons/project/views/rating_views.xml | 2 +- .../report_all_channels_sales_views.xml | 2 +- addons/sale/report/sale_report_views.xml | 2 +- addons/sale/views/sale_views.xml | 7 +++++++ addons/stock/views/stock_move_views.xml | 2 +- .../website_sale/views/sale_report_views.xml | 19 +------------------ 22 files changed, 36 insertions(+), 77 deletions(-) diff --git a/addons/account/report/account_invoice_report_view.xml b/addons/account/report/account_invoice_report_view.xml index 370176ba32e..c263b253d9b 100644 --- a/addons/account/report/account_invoice_report_view.xml +++ b/addons/account/report/account_invoice_report_view.xml @@ -74,9 +74,7 @@ - - - + diff --git a/addons/crm/report/crm_activity_report_views.xml b/addons/crm/report/crm_activity_report_views.xml index 8ddf6fcc63a..2e5c6642159 100644 --- a/addons/crm/report/crm_activity_report_views.xml +++ b/addons/crm/report/crm_activity_report_views.xml @@ -36,8 +36,7 @@ - - + diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml index 8ae35895a38..86058e9c5d5 100644 --- a/addons/crm/report/crm_opportunity_report_views.xml +++ b/addons/crm/report/crm_opportunity_report_views.xml @@ -101,8 +101,9 @@ - - + + + @@ -127,8 +128,9 @@ - + + diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index eed0b44ce05..e720acda11e 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -667,16 +667,13 @@ ]"/> - + + + - @@ -692,6 +689,7 @@ + diff --git a/addons/hr_attendance/views/hr_attendance_view.xml b/addons/hr_attendance/views/hr_attendance_view.xml index 6a715ef7f41..1997f8e415d 100644 --- a/addons/hr_attendance/views/hr_attendance_view.xml +++ b/addons/hr_attendance/views/hr_attendance_view.xml @@ -68,9 +68,7 @@ - - - + diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml index cda14788ae3..388120b0b6d 100644 --- a/addons/hr_recruitment/views/hr_recruitment_views.xml +++ b/addons/hr_recruitment/views/hr_recruitment_views.xml @@ -930,7 +930,7 @@ - + diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml index db2b06a5838..fefd0885d60 100644 --- a/addons/hr_timesheet/views/hr_timesheet_views.xml +++ b/addons/hr_timesheet/views/hr_timesheet_views.xml @@ -139,18 +139,7 @@ - - + diff --git a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml index d2b335d99ea..85d071cf51d 100644 --- a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml +++ b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report_view.xml @@ -7,18 +7,7 @@ - - + diff --git a/addons/im_livechat/report/im_livechat_report_channel_views.xml b/addons/im_livechat/report/im_livechat_report_channel_views.xml index 71fccb7c7c9..efe0304bd46 100644 --- a/addons/im_livechat/report/im_livechat_report_channel_views.xml +++ b/addons/im_livechat/report/im_livechat_report_channel_views.xml @@ -33,7 +33,7 @@ - + diff --git a/addons/im_livechat/report/im_livechat_report_operator_views.xml b/addons/im_livechat/report/im_livechat_report_operator_views.xml index fd7a5c4b422..2aa147ca1b0 100644 --- a/addons/im_livechat/report/im_livechat_report_operator_views.xml +++ b/addons/im_livechat/report/im_livechat_report_operator_views.xml @@ -31,7 +31,7 @@ - + diff --git a/addons/im_livechat/views/rating_views.xml b/addons/im_livechat/views/rating_views.xml index 7c9bdc2edd5..abaaf230080 100644 --- a/addons/im_livechat/views/rating_views.xml +++ b/addons/im_livechat/views/rating_views.xml @@ -12,7 +12,7 @@ - + diff --git a/addons/lunch/views/lunch_views.xml b/addons/lunch/views/lunch_views.xml index 6016d9a0bc4..90319d193fe 100644 --- a/addons/lunch/views/lunch_views.xml +++ b/addons/lunch/views/lunch_views.xml @@ -19,7 +19,7 @@ - + diff --git a/addons/mrp/views/mrp_workcenter_views.xml b/addons/mrp/views/mrp_workcenter_views.xml index 52bd2d3c331..12ba2d6376b 100644 --- a/addons/mrp/views/mrp_workcenter_views.xml +++ b/addons/mrp/views/mrp_workcenter_views.xml @@ -540,9 +540,7 @@ - - - + diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index f5afe548beb..e9b2de8cec1 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -6,16 +6,14 @@ - - - - + + diff --git a/addons/point_of_sale/views/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml index 243e93a3e0e..a6dcede51cf 100644 --- a/addons/point_of_sale/views/pos_order_report_view.xml +++ b/addons/point_of_sale/views/pos_order_report_view.xml @@ -33,7 +33,7 @@ - + diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml index 6a5feca3a0a..1175403a9a2 100644 --- a/addons/point_of_sale/views/pos_session_view.xml +++ b/addons/point_of_sale/views/pos_session_view.xml @@ -190,7 +190,7 @@ - + diff --git a/addons/project/views/rating_views.xml b/addons/project/views/rating_views.xml index 724517b9471..cd505238467 100644 --- a/addons/project/views/rating_views.xml +++ b/addons/project/views/rating_views.xml @@ -12,7 +12,7 @@ - + diff --git a/addons/sale/report/report_all_channels_sales_views.xml b/addons/sale/report/report_all_channels_sales_views.xml index 201e229a186..4b146e5f894 100644 --- a/addons/sale/report/report_all_channels_sales_views.xml +++ b/addons/sale/report/report_all_channels_sales_views.xml @@ -17,7 +17,7 @@ - + diff --git a/addons/sale/report/sale_report_views.xml b/addons/sale/report/sale_report_views.xml index 80920e180d4..70b2954ceaf 100644 --- a/addons/sale/report/sale_report_views.xml +++ b/addons/sale/report/sale_report_views.xml @@ -30,7 +30,7 @@ - + diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 0cc04eefd4c..22e7cedbf67 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -475,6 +475,9 @@ + + + @@ -489,6 +492,10 @@ + + + + diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index 6e86c920900..3bdb4f62b73 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -324,7 +324,7 @@ - + diff --git a/addons/website_sale/views/sale_report_views.xml b/addons/website_sale/views/sale_report_views.xml index 61b04df2c2b..6b8d7c4d9da 100644 --- a/addons/website_sale/views/sale_report_views.xml +++ b/addons/website_sale/views/sale_report_views.xml @@ -5,24 +5,7 @@ sale.report - - - + From 11b7857af63a35e81daa495bcb3522b3915f32ca Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Thu, 5 Jul 2018 17:09:33 +0200 Subject: [PATCH 11/11] [DOC] web: improve documentation of search view --- doc/reference/views.rst | 77 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/doc/reference/views.rst b/doc/reference/views.rst index eca2a0ad73f..9093bc314c6 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -1423,12 +1423,85 @@ Possible children elements of the search view are: ``string`` (required) the label of the filter - ``domain`` + ``domain`` (optional) an Odoo :ref:`domain `, will be appended to the - action's domain as part of the search domain + action's domain as part of the search domain. + ``date`` (optional) + the name of a field of type ``date`` or ``datetime``. + Using this attribute has the effect to create + a set of filters available in a submenu + of the filters menu. + + Example: + + .. code-block:: xml + + + + The example above allows to easily search for records with creation date field + values in one of the periods below. + + .. code-block:: text + + Create Date > + Today + This Week + This Month + This Quarter + This Year + -------------- + Yesterday + Last Week + Last Month + Last Quarter + Last Year + -------------- + Last 7 Days + Last 30 Days + Last 365 Days + + Note that the generated domains are dynamic and can be saved as such (via the favorites menu). + + ``default_period`` (optional) + only makes sense for a filter with non empty ``date`` attribute. + determines which period is activated if the filter is in the + default set of filters activated at the view initialization. If not provided, + 'this_month' is used by default. + + To choose among the following options: + today, this_week, this_month, this_quarter, this_year, + yesterday, last_week, last_month, + last_quarter, last_year, last_7_days, last_30_days, last_365_days + + Example: + + .. code-block:: xml + + + ``context`` a Python dictionary, merged into the action's domain to generate the search domain + + The key ``group_by`` can be used to define a groupby available in the + 'Group By' menu. + The 'group_by' value can be a valid field name or a list of field names. + + .. code-block:: xml + + + + The groupby defined above allows to group data by category. + + When the field is of type ``date`` or ``datetime``, the records are grouped by month by default. + This can be modified by using one of the following options: day, week, quarter, year. + + Example: + + .. code-block:: xml + + + ``name`` logical name for the filter, can be used to :ref:`enable it by default `, can also be used as