From 9132b1d3066771704e629c7303c9c339415191d4 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 30 Jun 2014 19:15:30 +0200 Subject: [PATCH] [IMP] ir.filters: new filters are local to the menu/action by default Allow binding an optional `action_id` to filters. The web client will try to identify the specific action ID when saving new filters. If no contextual action exists, the filter is saved globally for the model. This will automatically keep filters within their original menu when there are several menus/actions leading to a given list of documents. In some cases the action_id will not match the filter model, which should be fine (e.g. when opening a many2one completion popup for model `foo` within a menu of model `bar`). It is also still be possible to have a filter apply to all actions/menus for a given model by manually deleting the action_id value in the filter (e.g. via the Manage Filters debug menu). When updating a filter the action_id value is ignored so that old global filters will be gradually replaced by new "local" filters. Also added an _order to ensure stable ordering of the filters. --- addons/web/static/src/js/search.js | 12 +++- addons/web/static/test/search.js | 1 + openerp/addons/base/ir/ir_filters.py | 58 +++++++++++++++----- openerp/addons/base/ir/ir_filters.xml | 2 + openerp/addons/base/tests/test_ir_filters.py | 7 +-- 5 files changed, 58 insertions(+), 22 deletions(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 3fce58fe0bb..4b00f6f4e01 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1710,11 +1710,15 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ self.clear_selection(); }) .on('reset', this.proxy('clear_selection')); - return this.model.call('get_filters', [this.view.model]) + return this.model.call('get_filters', [this.view.model, this.get_action_id()]) .then(this.proxy('set_filters')) .done(function () { self.is_ready.resolve(); }) .fail(function () { self.is_ready.reject.apply(self.is_ready, arguments); }); }, + get_action_id: function(){ + var action = instance.client.action_manager.inner_action; + if (action) return action.id; + }, /** * Special implementation delaying defaults until CustomFilters is loaded */ @@ -1734,9 +1738,11 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ * @return {String} mapping key corresponding to the filter */ key_for: function (filter) { - var user_id = filter.user_id; + var user_id = filter.user_id, + action_id = filter.action_id; var uid = (user_id instanceof Array) ? user_id[0] : user_id; - return _.str.sprintf('(%s)%s', uid, filter.name); + var act_id = (action_id instanceof Array) ? action_id[0] : action_id; + return _.str.sprintf('(%s)(%s)%s', uid, act_id, filter.name); }, /** * Generates a :js:class:`~instance.web.search.Facet` descriptor from a diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index 5faf13b2599..d93cb131e05 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -171,6 +171,7 @@ var makeSearchView = function (instance, dummy_widget_attributes, defaults) { dummy: {type: 'char', string: 'Dummy'} }; }; + instance.client = { action_manager: { inner_action: undefined } }; var dataset = new instance.web.DataSet(null, 'dummy.model'); var mock_parent = {getParent: function () {return null;}}; diff --git a/openerp/addons/base/ir/ir_filters.py b/openerp/addons/base/ir/ir_filters.py index 6364a2492be..f179a8169f0 100644 --- a/openerp/addons/base/ir/ir_filters.py +++ b/openerp/addons/base/ir/ir_filters.py @@ -36,15 +36,30 @@ class ir_filters(osv.osv): default.update({'name':_('%s (copy)') % name}) return super(ir_filters, self).copy(cr, uid, id, default, context) - def get_filters(self, cr, uid, model): + def _get_action_domain(self, cr, uid, action_id=None): + """Return a domain component for matching filters that are visible in the + same context (menu/view) as the given action.""" + if action_id: + # filters specific to this menu + global ones + return [('action_id', 'in' , [action_id, False])] + # only global ones + return [('action_id', '=', False)] + + def get_filters(self, cr, uid, model, action_id=None): """Obtain the list of filters available for the user on the given model. + :param action_id: optional ID of action to restrict filters to this action + plus global filters. If missing only global filters are returned. + The action does not have to correspond to the model, it may only be + a contextual action. :return: list of :meth:`~osv.read`-like dicts containing the - ``name``, ``is_default``, ``domain``, ``user_id`` (m2o tuple) and - ``context`` of the matching ``ir.filters``. + ``name``, ``is_default``, ``domain``, ``user_id`` (m2o tuple), + ``action_id`` (m2o tuple) and ``context`` of the matching ``ir.filters``. """ - # available filters: private filters (user_id=uid) and public filters (uid=NULL) - filter_ids = self.search(cr, uid, + # available filters: private filters (user_id=uid) and public filters (uid=NULL), + # and filters for the action (action_id=action_id) or global (action_id=NULL) + action_domain = self._get_action_domain(cr, uid, action_id) + filter_ids = self.search(cr, uid, action_domain + [('model_id','=',model),('user_id','in',[uid, False])]) my_filters = self.read(cr, uid, filter_ids, ['name', 'is_default', 'domain', 'context', 'user_id']) @@ -66,7 +81,8 @@ class ir_filters(osv.osv): :raises openerp.exceptions.Warning: if there is an existing default and we're not updating it """ - existing_default = self.search(cr, uid, [ + action_domain = self._get_action_domain(cr, uid, vals.get('action_id')) + existing_default = self.search(cr, uid, action_domain + [ ('model_id', '=', vals['model_id']), ('user_id', '=', False), ('is_default', '=', True)], context=context) @@ -83,7 +99,9 @@ class ir_filters(osv.osv): def create_or_replace(self, cr, uid, vals, context=None): lower_name = vals['name'].lower() - matching_filters = [f for f in self.get_filters(cr, uid, vals['model_id']) + action_id = vals.get('action_id') + current_filters = self.get_filters(cr, uid, vals['model_id'], action_id) + matching_filters = [f for f in current_filters if f['name'].lower() == lower_name # next line looks for matching user_ids (specific or global), i.e. # f.user_id is False and vals.user_id is False or missing, @@ -92,18 +110,22 @@ class ir_filters(osv.osv): if vals.get('is_default'): if vals.get('user_id'): - act_ids = self.search(cr, uid, [ + # Setting new default: any other default that belongs to the user + # should be turned off + action_domain = self._get_action_domain(cr, uid, action_id) + act_ids = self.search(cr, uid, action_domain + [ ('model_id', '=', vals['model_id']), ('user_id', '=', vals['user_id']), ('is_default', '=', True), ], context=context) - self.write(cr, uid, act_ids, {'is_default': False}, context=context) + if act_ids: + self.write(cr, uid, act_ids, {'is_default': False}, context=context) else: self._check_global_default( cr, uid, vals, matching_filters, context=None) # When a filter exists for the same (name, model, user) triple, we simply - # replace its definition. + # replace its definition (considering action_id irrelevant here) if matching_filters: self.write(cr, uid, matching_filters[0]['id'], vals, context) return matching_filters[0]['id'] @@ -114,16 +136,17 @@ class ir_filters(osv.osv): # Partial constraint, complemented by unique index (see below) # Still useful to keep because it provides a proper error message when a violation # occurs, as it shares the same prefix as the unique index. - ('name_model_uid_unique', 'unique (name, model_id, user_id)', 'Filter names must be unique'), + ('name_model_uid_unique', 'unique (name, model_id, user_id, action_id)', 'Filter names must be unique'), ] def _auto_init(self, cr, context=None): super(ir_filters, self)._auto_init(cr, context) # Use unique index to implement unique constraint on the lowercase name (not possible using a constraint) - cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_index'") + cr.execute("DROP INDEX IF EXISTS ir_filters_name_model_uid_unique_index") # drop old index w/o action + cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_action_index'") if not cr.fetchone(): - cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_index" ON ir_filters - (lower(name), model_id, COALESCE(user_id,-1))""") + cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_action_index" ON ir_filters + (lower(name), model_id, COALESCE(user_id,-1), COALESCE(action_id,-1))""") _columns = { 'name': fields.char('Filter Name', translate=True, required=True), @@ -133,7 +156,11 @@ class ir_filters(osv.osv): 'domain': fields.text('Domain', required=True), 'context': fields.text('Context', required=True), 'model_id': fields.selection(_list_all_models, 'Model', required=True), - 'is_default': fields.boolean('Default filter') + 'is_default': fields.boolean('Default filter'), + 'action_id': fields.many2one('ir.actions.actions', 'Action', ondelete='cascade', + help="The menu action this filter applies to. " + "When left empty the filter applies to all menus " + "for this model.") } _defaults = { 'domain': '[]', @@ -141,5 +168,6 @@ class ir_filters(osv.osv): 'user_id': lambda self,cr,uid,context=None: uid, 'is_default': False } + _order = 'model_id, name, id desc' # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_filters.xml b/openerp/addons/base/ir/ir_filters.xml index 46242b67ab4..acc16e172b4 100644 --- a/openerp/addons/base/ir/ir_filters.xml +++ b/openerp/addons/base/ir/ir_filters.xml @@ -20,6 +20,7 @@ + @@ -37,6 +38,7 @@ + diff --git a/openerp/addons/base/tests/test_ir_filters.py b/openerp/addons/base/tests/test_ir_filters.py index 04ecca74525..9ca093d77af 100644 --- a/openerp/addons/base/tests/test_ir_filters.py +++ b/openerp/addons/base/tests/test_ir_filters.py @@ -5,10 +5,9 @@ from openerp import exceptions from openerp.tests import common def noid(d): - """ Removes `id` key from a dict so we don't have to keep these things - around when trying to match - """ - if 'id' in d: del d['id'] + """ Removes values that are not relevant for the test comparisons """ + d.pop('id', None) + d.pop('action_id', None) return d class FiltersCase(common.TransactionCase):