[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.
This commit is contained in:
Olivier Dony 2014-06-30 19:15:30 +02:00
parent 686fea4b3e
commit 9132b1d306
5 changed files with 58 additions and 22 deletions

View File

@ -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

View File

@ -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;}};

View File

@ -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:

View File

@ -20,6 +20,7 @@
<field name="user_id"/>
<field name="model_id"/>
<field name="is_default"/>
<field name="action_id"/>
</group>
<group>
<field name="domain"/>
@ -37,6 +38,7 @@
<field name="model_id"/>
<field name="user_id"/>
<field name="is_default"/>
<field name="action_id"/>
<field name="domain" groups="base.group_no_one"/>
<field name="context" groups="base.group_no_one"/>
</tree>

View File

@ -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):