diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 4417912b6d4..2a7de8d81d0 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1009,20 +1009,26 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc values: [{label: String(value), value: value}] }); }, - get_value: function (facet) { - return facet.value(); + value_from: function (facetValue) { + return facetValue.get('value'); }, get_context: function (facet) { - // A field needs a value to be "active", and a context to send when - // active + var self = this; + // A field needs a context to send when active var context = this.attrs.context; - if (!context) { + if (!context || !facet.values.length) { return; } - var val = this.get_value(facet); - var has_value = (val !== null && val !== ''); - return new instance.web.CompoundContext(context) - .set_eval_context({self: val}); + var contexts = facet.values.map(function (facetValue) { + return new instance.web.CompoundContext(context) + .set_eval_context({self: self.value_from(facetValue)}); + }); + + if (contexts.length === 1) { return contexts[0]; } + + return _.extend(instance.web.CompoundContext, { + __contexts: contexts + }); }, get_groupby: function () { }, /** @@ -1037,23 +1043,37 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc * @returns {Array} domain to include in the resulting search */ make_domain: function (name, operator, facet) { - return [[name, operator, this.get_value(facet)]]; + return [[name, operator, this.value_from(facet)]]; }, get_domain: function (facet) { - var val = this.get_value(facet); - if (val === null || val === '') { - return; + if (!facet.values.length) { return; } + + var value_to_domain; + var self = this; + var domain = this.attrs['filter_domain']; + if (domain) { + value_to_domain = function (facetValue) { + return new instance.web.CompoundDomain(domain) + .set_eval_context({self: self.value_from(facetValue)}); + }; + } else { + value_to_domain = function (facetValue) { + return self.make_domain( + self.attrs.name, + self.attrs.operator || self.default_operator, + facetValue); + }; + } + var domains = facet.values.map(value_to_domain); + + if (domains.length === 1) { return domains[0]; } + for (var i = domains.length; --i;) { + domains.unshift(['|']); } - var domain = this.attrs['filter_domain']; - if (!domain) { - return this.make_domain( - this.attrs.name, - this.attrs.operator || this.default_operator, - facet); - } - return new instance.web.CompoundDomain(domain) - .set_eval_context({self: val}); + return _.extend(new instance.web.CompoundDomain, { + __domains: domains + }); } }); /** @@ -1085,7 +1105,7 @@ instance.web.search.CharField = instance.web.search.Field.extend( /** @lends ins } }); instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{ - get_value: function () { + value_from: function () { if (!this.$element.val()) { return null; } @@ -1193,7 +1213,7 @@ instance.web.search.SelectionField = instance.web.search.Field.extend(/** @lends if (!match) { return $.when(null); } return $.when(facet_from(this, match)); }, - get_value: function (facet) { + value_from: function (facet) { return facet.get('values'); } }); @@ -1209,7 +1229,7 @@ instance.web.search.BooleanField = instance.web.search.SelectionField.extend(/** ['false', _t("No")] ]; }, - get_value: function (facet) { + value_from: function (facet) { switch (this._super(facet)) { case 'false': return false; case 'true': return true; @@ -1222,7 +1242,7 @@ instance.web.search.BooleanField = instance.web.search.SelectionField.extend(/** * @extends instance.web.search.DateField */ instance.web.search.DateField = instance.web.search.Field.extend(/** @lends instance.web.search.DateField# */{ - get_value: function (facet) { + value_from: function (facet) { return openerp.web.date_to_str(facet.get('values')); }, complete: function (needle) { @@ -1255,7 +1275,7 @@ instance.web.search.DateField = instance.web.search.Field.extend(/** @lends inst * @extends instance.web.DateField */ instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @lends instance.web.search.DateTimeField# */{ - get_value: function (facet) { + value_from: function (facet) { return openerp.web.datetime_to_str(facet.get('values')); } }); diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index 17fdfc81749..682cf27a7ab 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -770,6 +770,70 @@ $(document).ready(function () { }); }); + test('Field single value, default domain & context', function () { + var f = new instance.web.search.Field({}, {name: 'foo'}, {inputs: []}); + var facet = new instance.web.search.Facet({ + field: f, + values: [{value: 42}] + }); + + deepEqual(f.get_domain(facet), [['foo', '=', 42]], + "default field domain is a strict equality of name to facet's value"); + equal(f.get_context(facet), null, + "default field context is null"); + }); + test('Field multiple values, default domain & context', function () { + var f = new instance.web.search.Field({}, {name: 'foo'}, {inputs: []}); + var facet = new instance.web.search.Facet({ + field: f, + values: [{value: 42}, {value: 68}, {value: 999}] + }); + + var actual_domain = f.get_domain(facet); + equal(actual_domain.__ref, "compound_domain", + "multiple value should yield compound domain"); + deepEqual(actual_domain.__domains, [ + ['|'], + ['|'], + [['foo', '=', 42]], + [['foo', '=', 68]], + [['foo', '=', 999]] + ], + "domain should OR a default domain for each value"); + equal(f.get_context(facet), null, + "default field context is null"); + }); + test('Field single value, custom domain & context', function () { + var f = new instance.web.search.Field({attrs:{ + context: "{'bob': self}", + filter_domain: "[['edmund', 'is', self]]" + }}, {name: 'foo'}, {inputs: []}); + var facet = new instance.web.search.Facet({ + field: f, + values: [{value: "great"}] + }); + + var actual_domain = f.get_domain(facet); + equal(actual_domain.__ref, "compound_domain", + "@filter_domain should yield compound domain"); + deepEqual(actual_domain.__domains, [ + "[['edmund', 'is', self]]" + ], 'should hold unevaluated custom domain'); + deepEqual(actual_domain.get_eval_context(), { + self: "great" + }, "evaluation context should hold facet value as self"); + + var actual_context = f.get_context(facet); + equal(actual_context.__ref, "compound_context", + "@context should yield compound context"); + deepEqual(actual_context.__contexts, [ + "{'bob': self}" + ], 'should hold unevaluated custom context'); + deepEqual(actual_context.get_eval_context(), { + self: "great" + }, "evaluation context should hold facet value as self"); + }); + module('drawer', { setup: function () { instance = window.openerp.init([]); diff --git a/doc/search-view.rst b/doc/search-view.rst index 3c13f6ae921..f8dce473382 100644 --- a/doc/search-view.rst +++ b/doc/search-view.rst @@ -277,6 +277,92 @@ exception if the value is not valid at all for the field. ``Array`` of groupby domains rather than a single context. At this point, it is only implemented on (and used by) filters. +Field services +++++++++++++++ + +:js:class:`~openerp.web.search.Field` provides a default +implementation of :js:func:`~openerp.web.search.Input.get_domain` and +:js:func:`~openerp.web.search.Input.get_context` taking care of most +of the peculiarities pertaining to OpenERP's handling of fields in +search views. It also provides finer hooks to let developers of new +fields and widgets customize the behavior they want without +necessarily having to reimplement all of +:js:func:`~openerp.web.search.Input.get_domain` or +:js:func:`~openerp.web.search.Input.get_context`: + +.. js:function:: openerp.web.search.Field.get_context(facet) + + If the field has no ``@context``, simply returns + ``null``. Otherwise, calls + :js:func:`~openerp.web.search.Field.value_from` once for each + :js:class:`~openerp.web.search.FacetValue` of the current + :js:class:`~openerp.web.search.Facet` (in order to extract the + basic javascript object from the + :js:class:`~openerp.web.search.FacetValue` then evaluates + ``@context`` with each of these values set as ``self``, and + returns the union of all these contexts. + + :param facet: + :type facet: openerp.web.search.Facet + :returns: a context (literal or compound) + +.. js:function:: openerp.web.search.Field.get_domain(facet) + + If the field has no ``@filter_domain``, calls + :js:func:`~openerp.web.search.Field.make_domain` once with each + :js:class:`~openerp.web.search.FacetValue` of the current + :js:class:`~openerp.web.search.Facet` as well as the field's + ``@name`` and either its ``@operator`` or + :js:attr:`~openerp.web.search.Field.default_operator`. + + If the field has an ``@filter_value``, calls + :js:func:`~openerp.web.search.Field.value_from` once per + :js:class:`~openerp.web.search.FacetValue` and evaluates + ``@filter_value`` with each of these values set as ``self``. + + In either case, "ors" all of the resulting domains (using ``|``) + if there is more than one + :js:class:`~openerp.web.search.FacetValue` and returns the union + of the result. + + :param facet: + :type facet: openerp.web.search.Facet + :returns: a domain (literal or compound) + +.. js:function:: openerp.web.search.Field.make_domain(name, operator, facetValue) + + Builds a literal domain from the provided data. Calls + :js:func:`~openerp.web.search.Field.value_from` on the + :js:class:`~openerp.web.search.FacetValue` and evaluates and sets + it as the domain's third value, uses the other two parameters as + the first two values. + + Can be overridden to build more complex default domains. + + :param String name: the field's name + :param String operator: the operator to use in the field's domain + :param facetValue: + :type facetValue: openerp.web.search.FacetValue + :returns: Array<(String, String, Object)> + +.. js:function:: openerp.web.search.Field.value_from(facetValue) + + Extracts a "bare" javascript value from the provided + :js:class:`~openerp.web.search.FacetValue`, and returns it. + + The default implementation will simply return the ``value`` + backbone property of the argument. + + :param facetValue: + :type facetValue: openerp.web.search.FacetValue + :returns: Object + +.. js:attribute:: openerp.web.search.Field.default_operator + + Operator used to build a domain when a field has no ``@operator`` + or ``@filter_domain``. ``"="`` for + :js:class:`~openerp.web.search.Field` + Converting to facet objects --------------------------- @@ -314,7 +400,7 @@ Widgets API * :js:func:`~openerp.web.search.Input.get_domain` and :js:func:`~openerp.web.search.Input.get_context` now take a - :js:class:`~VS.model.SearchFacet` as parameter, from which it's + :js:class:`~openerp.web.search.Facet` as parameter, from which it's their job to get whatever value they want * :js:func:`~openerp.web.search.Input.get_groupby` has been added. It returns @@ -334,19 +420,16 @@ Filters Fields ++++++ -* ``get_value`` now takes a :js:class:`~VS.model.SearchFacet` (instead - of taking no argument). - - A default implementation is provided as - :js:func:`openerp.web.search.Field.get_value` and simply calls - :js:func:`VS.model.SearchFacet.value`. +* ``get_value`` has been replaced by + :js:func:`~openerp.web.search.Field.value_from` as it now takes a + :js:class:`~openerp.web.search.FacetValue` argument (instead of no + argument). It provides a default implementation returning the + ``value`` property of its argument. * The third argument to - :js:func:`~openerp.web.search.Field.make_domain` is now the - :js:class:`~VS.model.SearchFacet` received by - :js:func:`~openerp.web.search.Field.get_domain`, so child classes - have all the information they need to derive the "right" resulting - domain. + :js:func:`~openerp.web.search.Field.make_domain` is now a + :js:class:`~openerp.web.search.FacetValue` so child classes have all + the information they need to derive the "right" resulting domain. Custom filters ++++++++++++++ @@ -363,6 +446,13 @@ Many To One :js:func:`openerp.web.search.ManyToOneField.setup_autocomplete` has been removed. +Advanced Search ++++++++++++++++ + +The advanced search is now a more standard +:js:class:`~openerp.web.search.Input` configured to be rendered in the +drawer. + .. [#] the original view was implemented on top of a monkey-patched VisualSearch, but as our needs diverged from VisualSearch's goal this made less and less sense ultimately leading to a clean-room