[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:
parent
686fea4b3e
commit
9132b1d306
|
@ -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
|
||||
|
|
|
@ -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;}};
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue