diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 2227aa52465..4417912b6d4 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -656,7 +656,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea do_search: function () { var domains = [], contexts = [], groupbys = [], errors = []; - return this.on_search([], [], []); this.query.each(function (facet) { var field = facet.get('field'); try { @@ -902,12 +901,12 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in /** * Fetches contexts for all enabled filters in the group * - * @param {VS.model.SearchFacet} facet + * @param {openerp.web.search.Facet} facet * @return {*} combined contexts of the enabled filters in this group */ get_context: function (facet) { - var contexts = _(facet.get('values')).chain() - .map(function (filter) { return filter.attrs.context; }) + var contexts = _(facet.values).chain() + .map(function (f) { return f.get('value').attrs.context; }) .reject(_.isEmpty) .value(); @@ -924,8 +923,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in * @return {Array} enabled filters in this group */ get_groupby: function (facet) { - return _(facet.get('values')).chain() - .map(function (filter) { return filter.attrs.context; }) + return _(facet.values).chain() + .map(function (f) { return f.get('value').attrs.context; }) .reject(_.isEmpty) .value(); }, @@ -936,8 +935,8 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in * @return {*} combined domains of the enabled filters in this group */ get_domain: function (facet) { - var domains = _(facet.get('values')).chain() - .map(function (filter) { return filter.attrs.domain; }) + var domains = _(facet.values).chain() + .map(function (f) { return f.get('value').attrs.domain; }) .reject(_.isEmpty) .value(); @@ -1014,14 +1013,14 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc return facet.value(); }, get_context: function (facet) { - var val = this.get_value(facet); // A field needs a value to be "active", and a context to send when // active - var has_value = (val !== null && val !== ''); var context = this.attrs.context; - if (!(has_value && context)) { + if (!context) { return; } + var val = this.get_value(facet); + var has_value = (val !== null && val !== ''); return new instance.web.CompoundContext(context) .set_eval_context({self: val}); }, diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index 1dcf03f90bd..17fdfc81749 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -4,6 +4,25 @@ $(document).ready(function () { xhr.send(null); var doc = xhr.responseXML; + var noop = function () {}; + /** + * Make connection RPC responses mockable by setting keys on the + * Connection#responses object (key is the URL, value is the function to + * call with the RPC request payload) + * + * @param {openerp.web.Connection} connection connection instance to mockify + * @param {Object} [responses] url:function mapping to seed the mock connection + */ + var mockifyRPC = function (connection, responses) { + connection.responses = responses || {}; + connection.rpc_function = function (url, payload) { + if (!(url.url in this.responses)) { + return $.Deferred().reject({}, 'failed', _.str.sprintf("Url %s not found in mock responses", url.url)).promise(); + } + return $.when(this.responses[url.url](payload)); + }; + }; + var instance; module('query', { setup: function () { @@ -157,16 +176,7 @@ $(document).ready(function () { instance.web.qweb.add_template(doc); - instance.connection.responses = {}; - instance.connection.rpc_function = function (url, payload) { - if (!(url.url in this.responses)) { - return $.Deferred().reject( - {}, 'failed', - _.str.sprintf("Url %s not found in mock responses", - url.url)).promise(); - } - return $.when(this.responses[url.url](payload)); - }; + mockifyRPC(instance.connection); } }); @@ -220,7 +230,11 @@ $(document).ready(function () { }; var dataset = {model: 'dummy.model', get_context: function () { return {}; }}; - return new instance.web.SearchView(null, dataset, false, defaults); + var view = new instance.web.SearchView(null, dataset, false, defaults); + view.on_invalid.add(function () { + ok(false, JSON.stringify([].slice(arguments))); + }); + return view; } asyncTest('calling', 2, function () { var defaults_called = false; @@ -401,16 +415,7 @@ $(document).ready(function () { instance.web.qweb.add_template(doc); - instance.connection.responses = {}; - instance.connection.rpc_function = function (url, payload) { - if (!(url.url in this.responses)) { - return $.Deferred().reject( - {}, 'failed', - _.str.sprintf("Url %s not found in mock responses", - url.url)).promise(); - } - return $.when(this.responses[url.url](payload)); - }; + mockifyRPC(instance.connection); } }); asyncTest('calling', 4, function () { @@ -449,7 +454,7 @@ $(document).ready(function () { var completion = { label: "Dummy", facet: { - field: {}, + field: {get_domain: noop, get_context: noop, get_groupby: noop}, category: 'Dummy', values: [{label: 'dummy', value: 42}] } @@ -471,7 +476,7 @@ $(document).ready(function () { }); }); asyncTest('facet selection: new value existing facet', 3, function () { - var field = {}; + var field = {get_domain: noop, get_context: noop, get_groupby: noop}; var completion = { label: "Dummy", facet: { @@ -656,6 +661,115 @@ $(document).ready(function () { }); }); + module('search-serialization', { + setup: function () { + instance = window.openerp.init([]); + window.openerp.web.corelib(instance); + window.openerp.web.coresetup(instance); + window.openerp.web.chrome(instance); + window.openerp.web.data(instance); + window.openerp.web.search(instance); + + instance.web.qweb.add_template(doc); + + mockifyRPC(instance.connection); + } + }); + asyncTest('No facet, no call', 6, function () { + var got_domain = false, got_context = false, got_groupby = false; + var $fix = $('#qunit-fixture'); + var view = makeSearchView({ + get_domain: function () { + got_domain = true; + return null; + }, + get_context: function () { + got_context = true; + return null; + }, + get_groupby: function () { + got_groupby = true; + return null; + } + }); + var ds, cs, gs; + view.on_search.add(function (d, c, g) { + ds = d, cs = c, gs = g; + }); + view.appendTo($fix) + .always(start) + .fail(function (error) { ok(false, error.message); }) + .done(function () { + view.do_search(); + ok(!got_domain, "no facet, should not have fetched domain"); + ok(_(ds).isEmpty(), "domains list should be empty"); + + ok(!got_context, "no facet, should not have fetched context"); + ok(_(cs).isEmpty(), "contexts list should be empty"); + + ok(!got_groupby, "no facet, should not have fetched groupby"); + ok(_(gs).isEmpty(), "groupby list should be empty"); + }) + }); + asyncTest('London, calling', 8, function () { + var got_domain = false, got_context = false, got_groupby = false; + var $fix = $('#qunit-fixture'); + var view = makeSearchView({ + get_domain: function (facet) { + equal(facet.get('category'), "dummy"); + deepEqual(facet.values.toJSON(), [{label: "42", value: 42}]); + got_domain = true; + return null; + }, + get_context: function () { + got_context = true; + return null; + }, + get_groupby: function () { + got_groupby = true; + return null; + } + }, {dummy: 42}); + var ds, cs, gs; + view.on_search.add(function (d, c, g) { + ds = d, cs = c, gs = g; + }); + view.appendTo($fix) + .always(start) + .fail(function (error) { ok(false, error.message); }) + .done(function () { + view.do_search(); + ok(got_domain, "should have fetched domain"); + ok(_(ds).isEmpty(), "domains list should be empty"); + + ok(got_context, "should have fetched context"); + ok(_(cs).isEmpty(), "contexts list should be empty"); + + ok(got_groupby, "should have fetched groupby"); + ok(_(gs).isEmpty(), "groupby list should be empty"); + }) + }); + asyncTest('Generate domains', 1, function () { + var $fix = $('#qunit-fixture'); + var view = makeSearchView({ + get_domain: function (facet) { + return facet.values.map(function (value) { + return ['win', '4', value.get('value')]; + }); + } + }, {dummy: 42}); + var ds; + view.on_search.add(function (d) { ds = d; }); + view.appendTo($fix) + .always(start) + .fail(function (error) { ok(false, error.message); }) + .done(function () { + view.do_search(); + deepEqual(ds, [[['win', '4', 42]]], + "search should yield an array of contexts"); + }); + }); + module('drawer', { setup: function () { instance = window.openerp.init([]); @@ -667,16 +781,7 @@ $(document).ready(function () { instance.web.qweb.add_template(doc); - instance.connection.responses = {}; - instance.connection.rpc_function = function (url, payload) { - if (!(url.url in this.responses)) { - return $.Deferred().reject( - {}, 'failed', - _.str.sprintf("Url %s not found in mock responses", - url.url)).promise(); - } - return $.when(this.responses[url.url](payload)); - }; + mockifyRPC(instance.connection); } }); asyncTest('is-drawn', 2, function () { @@ -704,43 +809,35 @@ $(document).ready(function () { instance.web.qweb.add_template(doc); - instance.connection.responses = {}; - instance.connection.rpc_function = function (url, payload) { - if (!(url.url in this.responses)) { - return $.Deferred().reject( - {}, 'failed', - _.str.sprintf("Url %s not found in mock responses", - url.url)).promise(); + mockifyRPC(instance.connection, { + '/web/searchview/load': function () { + // view with a single group of filters + return {result: {fields_view: { + type: 'search', + fields: {}, + arch: { + tag: 'search', + attrs: {}, + children: [{ + tag: 'filter', + attrs: { string: "Foo1", domain: [ ['foo', '=', '1'] ] }, + children: [] + }, { + tag: 'filter', + attrs: { + name: 'foo2', + string: "Foo2", + domain: [ ['foo', '=', '2'] ] }, + children: [] + }, { + tag: 'filter', + attrs: { string: "Foo3", domain: [ ['foo', '=', '3'] ] }, + children: [] + }] + } + }}}; } - return $.when(this.responses[url.url](payload)); - }; - instance.connection.responses['/web/searchview/load'] = function () { - // view with a single group of filters - return {result: {fields_view: { - type: 'search', - fields: {}, - arch: { - tag: 'search', - attrs: {}, - children: [{ - tag: 'filter', - attrs: { string: "Foo1", domain: [ ['foo', '=', '1'] ] }, - children: [] - }, { - tag: 'filter', - attrs: { - name: 'foo2', - string: "Foo2", - domain: [ ['foo', '=', '2'] ] }, - children: [] - }, { - tag: 'filter', - attrs: { string: "Foo3", domain: [ ['foo', '=', '3'] ] }, - children: [] - }] - } - }}}; - }; + }); } }); asyncTest('drawn', 3, function () { diff --git a/doc/search-view.rst b/doc/search-view.rst index a7fbb77f51b..3c13f6ae921 100644 --- a/doc/search-view.rst +++ b/doc/search-view.rst @@ -249,21 +249,33 @@ Converting from facet objects Ultimately, the point of the search view is to allow searching. In OpenERP this is done via :ref:`domains `. On the other hand, the OpenERP Web 7 search view's state is modelled -after a collection of :js:class:`~VS.model.SearchFacet`, and each +after a collection of :js:class:`~openerp.web.search.Facet`, and each field of a search view may have special requirements when it comes to the domains it produces [#]_. So there needs to be some way of mapping -:js:class:`~VS.model.SearchFacet` objects to OpenERP search data. +:js:class:`~openerp.web.search.Facet` objects to OpenERP search data. This is done via an input's :js:func:`~openerp.web.search.Input.get_domain` and :js:func:`~openerp.web.search.Input.get_context`. Each takes a -:js:class:`~VS.model.SearchFacet` and returns whatever it's supposed -to generate (a domain or a context, respectively). Either can return -``null`` if the current value does not map to a domain or context, and -can throw an :js:class:`~openerp.web.search.Invalid` exception if the -value is not valid at all for the field. +:js:class:`~openerp.web.search.Facet` and returns whatever it's +supposed to generate (a domain or a context, respectively). Either can +return ``null`` if the current value does not map to a domain or +context, and can throw an :js:class:`~openerp.web.search.Invalid` +exception if the value is not valid at all for the field. + +.. note:: + + The :js:class:`~openerp.web.search.Facet` object can have any + number of values (from 1 upwards) + +.. note:: + + There is a third conversion method, + :js:func:`~openerp.web.search.Input.get_groupby`, which returns an + ``Array`` of groupby domains rather than a single context. At this + point, it is only implemented on (and used by) filters. Converting to facet objects ---------------------------