diff --git a/openerp/addons/base/ir/ir_filters.py b/openerp/addons/base/ir/ir_filters.py index 02a723c116c..7c03341b55f 100644 --- a/openerp/addons/base/ir/ir_filters.py +++ b/openerp/addons/base/ir/ir_filters.py @@ -19,6 +19,7 @@ # ############################################################################## +from openerp import exceptions from osv import osv, fields from tools.translate import _ @@ -41,13 +42,47 @@ class ir_filters(osv.osv): def get_filters(self, cr, uid, model): """Obtain the list of filters available for the user on the given model. - :return: list of :meth:`~osv.read`-like dicts containing the ``name``, - ``domain``, ``user_id`` (m2o tuple) and ``context`` of the matching ``ir.filters``. + :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``. """ # available filters: private filters (user_id=uid) and public filters (uid=NULL) - act_ids = self.search(cr, uid, [('model_id','=',model),('user_id','in',[uid, False])]) - my_acts = self.read(cr, uid, act_ids, ['name', 'domain', 'context', 'user_id']) - return my_acts + filter_ids = self.search(cr, uid, + [('model_id','=',model),('user_id','in',[uid, False])]) + my_filters = self.read(cr, uid, filter_ids, + ['name', 'is_default', 'domain', 'context', 'user_id']) + return my_filters + + def _check_global_default(self, cr, uid, vals, matching_filters, context=None): + """ _check_global_default(cursor, UID, dict, list(dict), dict) -> None + + Checks if there is a global default for the model_id requested. + + If there is, and the default is different than the record being written + (-> we're not updating the current global default), raise an error + to avoid users unknowingly overwriting existing global defaults (they + have to explicitly remove the current default before setting a new one) + + This method should only be called if ``vals`` is trying to set + ``is_default`` + + :raises openerp.exceptions.Warning: if there is an existing default and + we're not updating it + """ + existing_default = self.search(cr, uid, [ + ('model_id', '=', vals['model_id']), + ('user_id', '=', False), + ('is_default', '=', True)], context=context) + + if not existing_default: return + if matching_filters and \ + (matching_filters[0]['id'] == existing_default[0]): + return + + raise exceptions.Warning( + _("There is already a shared filter set as default for %(model)s, delete or change it before setting a new default") % { + 'model': vals['model_id'] + }) def create_or_replace(self, cr, uid, vals, context=None): lower_name = vals['name'].lower() @@ -57,11 +92,25 @@ class ir_filters(osv.osv): # f.user_id is False and vals.user_id is False or missing, # or f.user_id.id == vals.user_id if (f['user_id'] and f['user_id'][0]) == vals.get('user_id', False)] + + if vals.get('is_default'): + if vals.get('user_id'): + act_ids = self.search(cr, uid, [ + ('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) + 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. if matching_filters: self.write(cr, uid, matching_filters[0]['id'], vals, context) return matching_filters[0]['id'] + return self.create(cr, uid, vals, context) _sql_constraints = [ @@ -87,11 +136,13 @@ 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') } _defaults = { 'domain': '[]', 'context':'{}', 'user_id': lambda self,cr,uid,context=None: uid, + 'is_default': False } ir_filters() diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index c4b77a606a9..65277218da3 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -11,9 +11,11 @@ See the :ref:`test-framework` section in the :ref:`features` list. from . import test_expression, test_html_sanitize, test_ir_sequence, test_orm,\ test_fields, test_basecase, \ test_view_validation, test_uninstall, test_misc, test_db_cursor +from . import test_ir_filters fast_suite = [ test_ir_sequence, + test_ir_filters ] checks = [ diff --git a/openerp/tests/common.py b/openerp/tests/common.py index 3cc8b126c02..2c492e915cb 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -25,7 +25,7 @@ if not DB and hasattr(threading.current_thread(), 'dbname'): HOST = '127.0.0.1' ADMIN_USER = 'admin' -ADMIN_USER_ID = 1 +ADMIN_USER_ID = openerp.SUPERUSER_ID ADMIN_PASSWORD = 'admin' def start_openerp(): diff --git a/openerp/tests/test_ir_filters.py b/openerp/tests/test_ir_filters.py new file mode 100644 index 00000000000..c6582db1cb2 --- /dev/null +++ b/openerp/tests/test_ir_filters.py @@ -0,0 +1,264 @@ +# -*- coding: utf-8 -*- +import functools + +import openerp +from openerp import exceptions +from . import common + +class Fixtures(object): + def __init__(self, *args): + self.fixtures = args + + def __call__(self, fn): + @functools.wraps(fn) + def wrapper(case): + for model, vars in self.fixtures: + case.registry(model).create( + case.cr, common.ADMIN_USER_ID, vars, {}) + + fn(case) + return wrapper +def fixtures(*args): + return Fixtures(*args) + +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'] + return d + +class TestGetFilters(common.TransactionCase): + USER_ID = 3 + USER = (3, u'Demo User') + + @fixtures( + ('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')), + ('ir.filters', dict(name='b', user_id=USER_ID, model_id='ir.filters')), + ('ir.filters', dict(name='c', user_id=USER_ID, model_id='ir.filters')), + ('ir.filters', dict(name='d', user_id=USER_ID, model_id='ir.filters')), + ) + def test_own_filters(self): + filters = self.registry('ir.filters').get_filters( + self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', is_default=False, user_id=self.USER, domain='[]', context='{}'), + dict(name='b', is_default=False, user_id=self.USER, domain='[]', context='{}'), + dict(name='c', is_default=False, user_id=self.USER, domain='[]', context='{}'), + dict(name='d', is_default=False, user_id=self.USER, domain='[]', context='{}'), + ]) + + @fixtures( + ('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')), + ('ir.filters', dict(name='b', user_id=False, model_id='ir.filters')), + ('ir.filters', dict(name='c', user_id=False, model_id='ir.filters')), + ('ir.filters', dict(name='d', user_id=False, model_id='ir.filters')), + ) + def test_global_filters(self): + filters = self.registry('ir.filters').get_filters( + self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', is_default=False, user_id=False, domain='[]', context='{}'), + dict(name='b', is_default=False, user_id=False, domain='[]', context='{}'), + dict(name='c', is_default=False, user_id=False, domain='[]', context='{}'), + dict(name='d', is_default=False, user_id=False, domain='[]', context='{}'), + ]) + + @fixtures( + ('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')), + ('ir.filters', dict(name='b', user_id=common.ADMIN_USER_ID, model_id='ir.filters')), + ('ir.filters', dict(name='c', user_id=USER_ID, model_id='ir.filters')), + ('ir.filters', dict(name='d', user_id=common.ADMIN_USER_ID, model_id='ir.filters')), + ) + def test_no_third_party_filters(self): + filters = self.registry('ir.filters').get_filters( + self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', is_default=False, user_id=False, domain='[]', context='{}'), + dict(name='c', is_default=False, user_id=self.USER, domain='[]', context='{}'), + ]) + +class TestOwnDefaults(common.TransactionCase): + USER_ID = 3 + USER = (3, u'Demo User') + + def test_new_no_filter(self): + """ + When creating a @is_default filter with no existing filter, that new + filter gets the default flag + """ + Filters = self.registry('ir.filters') + Filters.create_or_replace(self.cr, self.USER_ID, { + 'name': 'a', + 'model_id': 'ir.filters', + 'user_id': self.USER_ID, + 'is_default': True, + }) + filters = Filters.get_filters(self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', user_id=self.USER, is_default=True, + domain='[]', context='{}') + ]) + + @fixtures( + ('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')), + ('ir.filters', dict(name='b', user_id=USER_ID, model_id='ir.filters')), + ) + def test_new_filter_not_default(self): + """ + When creating a @is_default filter with existing non-default filters, + the new filter gets the flag + """ + Filters = self.registry('ir.filters') + Filters.create_or_replace(self.cr, self.USER_ID, { + 'name': 'c', + 'model_id': 'ir.filters', + 'user_id': self.USER_ID, + 'is_default': True, + }) + filters = Filters.get_filters(self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', user_id=self.USER, is_default=False, domain='[]', context='{}'), + dict(name='b', user_id=self.USER, is_default=False, domain='[]', context='{}'), + dict(name='c', user_id=self.USER, is_default=True, domain='[]', context='{}'), + ]) + + @fixtures( + ('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')), + ('ir.filters', dict(name='b', is_default=True, user_id=USER_ID, model_id='ir.filters')), + ) + def test_new_filter_existing_default(self): + """ + When creating a @is_default filter where an existing filter is already + @is_default, the flag should be *moved* from the old to the new filter + """ + Filters = self.registry('ir.filters') + Filters.create_or_replace(self.cr, self.USER_ID, { + 'name': 'c', + 'model_id': 'ir.filters', + 'user_id': self.USER_ID, + 'is_default': True, + }) + filters = Filters.get_filters(self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', user_id=self.USER, is_default=False, domain='[]', context='{}'), + dict(name='b', user_id=self.USER, is_default=False, domain='[]', context='{}'), + dict(name='c', user_id=self.USER, is_default=True, domain='[]', context='{}'), + ]) + + @fixtures( + ('ir.filters', dict(name='a', user_id=USER_ID, model_id='ir.filters')), + ('ir.filters', dict(name='b', is_default=True, user_id=USER_ID, model_id='ir.filters')), + ) + def test_update_filter_set_default(self): + """ + When updating an existing filter to @is_default, if an other filter + already has the flag the flag should be moved + """ + Filters = self.registry('ir.filters') + Filters.create_or_replace(self.cr, self.USER_ID, { + 'name': 'a', + 'model_id': 'ir.filters', + 'user_id': self.USER_ID, + 'is_default': True, + }) + filters = Filters.get_filters(self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', user_id=self.USER, is_default=True, domain='[]', context='{}'), + dict(name='b', user_id=self.USER, is_default=False, domain='[]', context='{}'), + ]) + +class TestGlobalDefaults(common.TransactionCase): + USER_ID = 3 + + @fixtures( + ('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')), + ('ir.filters', dict(name='b', user_id=False, model_id='ir.filters')), + ) + def test_new_filter_not_default(self): + """ + When creating a @is_default filter with existing non-default filters, + the new filter gets the flag + """ + Filters = self.registry('ir.filters') + Filters.create_or_replace(self.cr, self.USER_ID, { + 'name': 'c', + 'model_id': 'ir.filters', + 'user_id': False, + 'is_default': True, + }) + filters = Filters.get_filters(self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', user_id=False, is_default=False, domain='[]', context='{}'), + dict(name='b', user_id=False, is_default=False, domain='[]', context='{}'), + dict(name='c', user_id=False, is_default=True, domain='[]', context='{}'), + ]) + + @fixtures( + ('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')), + ('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')), + ) + def test_new_filter_existing_default(self): + """ + When creating a @is_default filter where an existing filter is already + @is_default, an error should be generated + """ + Filters = self.registry('ir.filters') + with self.assertRaises(exceptions.Warning): + Filters.create_or_replace(self.cr, self.USER_ID, { + 'name': 'c', + 'model_id': 'ir.filters', + 'user_id': False, + 'is_default': True, + }) + + @fixtures( + ('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')), + ('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')), + ) + def test_update_filter_set_default(self): + """ + When updating an existing filter to @is_default, if an other filter + already has the flag an error should be generated + """ + Filters = self.registry('ir.filters') + + with self.assertRaises(exceptions.Warning): + Filters.create_or_replace(self.cr, self.USER_ID, { + 'name': 'a', + 'model_id': 'ir.filters', + 'user_id': False, + 'is_default': True, + }) + + @fixtures( + ('ir.filters', dict(name='a', user_id=False, model_id='ir.filters')), + ('ir.filters', dict(name='b', is_default=True, user_id=False, model_id='ir.filters')), + ) + def test_update_default_filter(self): + """ + Replacing the current default global filter should not generate any error + """ + Filters = self.registry('ir.filters') + context_value = "{'some_key': True}" + Filters.create_or_replace(self.cr, self.USER_ID, { + 'name': 'b', + 'model_id': 'ir.filters', + 'user_id': False, + 'context': context_value, + 'is_default': True, + }) + filters = Filters.get_filters(self.cr, self.USER_ID, 'ir.filters') + + self.assertItemsEqual(map(noid, filters), [ + dict(name='a', user_id=False, is_default=False, domain='[]', context='{}'), + dict(name='b', user_id=False, is_default=True, domain='[]', context=context_value), + ]) \ No newline at end of file