diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 1a75c08c695c32a03362ae5bbb93e9fb23e1e1a8..fc0edc24676cc497aedfd853ef860836ef5f24f3 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -141,8 +141,23 @@ class view(osv.osv): 'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True), 'create_date': fields.datetime('Create Date', readonly=True), 'write_date': fields.datetime('Last Modification Date', readonly=True), + + 'mode': fields.selection( + [('primary', "Base view"), ('extension', "Extension View")], + string="View inheritance mode", required=True, + help="""Only applies if this view inherits from an other one (inherit_id is not False/Null). + +* if extension (default), if this view is requested the closest primary view + is looked up (via inherit_id), then all views inheriting from it with this + view's model are applied +* if primary, the closest primary view is fully resolved (even if it uses a + different model than this one), then this view's inheritance specs + (<xpath/>) are applied, and the result is used as if it were this view's + actual arch. +"""), } _defaults = { + 'mode': 'primary', 'priority': 16, } _order = "priority,name" @@ -191,8 +206,14 @@ class view(osv.osv): return False return True + _sql_constraints = [ + ('inheritance_mode', + "CHECK (mode != 'extension' OR inherit_id IS NOT NULL)", + "Invalid inheritance mode: if the mode is 'extension', the view must" + " extend an other view"), + ] _constraints = [ - (_check_xml, 'Invalid view definition', ['arch']) + (_check_xml, 'Invalid view definition', ['arch']), ] def _auto_init(self, cr, context=None): @@ -201,6 +222,12 @@ class view(osv.osv): if not cr.fetchone(): cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') + def _compute_defaults(self, cr, uid, values, context=None): + if 'inherit_id' in values: + values.setdefault( + 'mode', 'extension' if values['inherit_id'] else 'primary') + return values + def create(self, cr, uid, values, context=None): if 'type' not in values: if values.get('inherit_id'): @@ -209,10 +236,13 @@ class view(osv.osv): values['type'] = etree.fromstring(values['arch']).tag if not values.get('name'): - values['name'] = "%s %s" % (values['model'], values['type']) + values['name'] = "%s %s" % (values.get('model'), values['type']) self.read_template.clear_cache(self) - return super(view, self).create(cr, uid, values, context) + return super(view, self).create( + cr, uid, + self._compute_defaults(cr, uid, values, context=context), + context=context) def write(self, cr, uid, ids, vals, context=None): if not isinstance(ids, (list, tuple)): @@ -227,7 +257,10 @@ class view(osv.osv): self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids) self.read_template.clear_cache(self) - ret = super(view, self).write(cr, uid, ids, vals, context) + ret = super(view, self).write( + cr, uid, ids, + self._compute_defaults(cr, uid, vals, context=context), + context) return ret def copy(self, cr, uid, id, default=None, context=None): @@ -241,7 +274,7 @@ class view(osv.osv): # default view selection def default_view(self, cr, uid, model, view_type, context=None): """ Fetches the default view for the provided (model, view_type) pair: - view with no parent (inherit_id=Fase) with the lowest priority. + primary view with the lowest priority. :param str model: :param int view_type: @@ -251,7 +284,7 @@ class view(osv.osv): domain = [ ['model', '=', model], ['type', '=', view_type], - ['inherit_id', '=', False], + ['mode', '=', 'primary'], ] ids = self.search(cr, uid, domain, limit=1, context=context) if not ids: @@ -278,15 +311,18 @@ class view(osv.osv): user = self.pool['res.users'].browse(cr, 1, uid, context=context) user_groups = frozenset(user.groups_id or ()) - check_view_ids = context and context.get('check_view_ids') or (0,) - conditions = [['inherit_id', '=', view_id], ['model', '=', model]] + conditions = [ + ['inherit_id', '=', view_id], + ['model', '=', model], + ['mode', '=', 'extension'], + ] if self.pool._init: # Module init currently in progress, only consider views from # modules whose code is already loaded conditions.extend([ '|', ['model_ids.module', 'in', tuple(self.pool._init_modules)], - ['id', 'in', check_view_ids], + ['id', 'in', context and context.get('check_view_ids') or (0,)], ]) view_ids = self.search(cr, uid, conditions, context=context) @@ -442,7 +478,7 @@ class view(osv.osv): if context is None: context = {} if root_id is None: root_id = source_id - sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context) + sql_inherit = self.pool['ir.ui.view'].get_inheriting_views_arch(cr, uid, source_id, model, context=context) for (specs, view_id) in sql_inherit: specs_tree = etree.fromstring(specs.encode('utf-8')) if context.get('inherit_branding'): @@ -465,7 +501,7 @@ class view(osv.osv): # if view_id is not a root view, climb back to the top. base = v = self.browse(cr, uid, view_id, context=context) - while v.inherit_id: + while v.mode != 'primary': v = v.inherit_id root_id = v.id @@ -475,7 +511,16 @@ class view(osv.osv): # read the view arch [view] = self.read(cr, uid, [root_id], fields=fields, context=context) - arch_tree = etree.fromstring(view['arch'].encode('utf-8')) + view_arch = etree.fromstring(view['arch'].encode('utf-8')) + if not v.inherit_id: + arch_tree = view_arch + else: + parent_view = self.read_combined( + cr, uid, v.inherit_id.id, fields=fields, context=context) + arch_tree = etree.fromstring(parent_view['arch']) + self.apply_inheritance_specs( + cr, uid, arch_tree, view_arch, parent_view['id'], context=context) + if context.get('inherit_branding'): arch_tree.attrib.update({ diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index feb22eb2be9a17226704b3d0a13fd32618404450..07cd4715b83ec9809339ffef4875d82d479836e7 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -1,12 +1,16 @@ # -*- encoding: utf-8 -*- from functools import partial +import itertools import unittest2 from lxml import etree as ET from lxml.builder import E +from psycopg2 import IntegrityError + from openerp.tests import common +import openerp.tools Field = E.field @@ -14,9 +18,15 @@ class ViewCase(common.TransactionCase): def setUp(self): super(ViewCase, self).setUp() self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual) + self.Views = self.registry('ir.ui.view') + + def browse(self, id, context=None): + return self.Views.browse(self.cr, self.uid, id, context=context) + def create(self, value, context=None): + return self.Views.create(self.cr, self.uid, value, context=context) def assertTreesEqual(self, n1, n2, msg=None): - self.assertEqual(n1.tag, n2.tag) + self.assertEqual(n1.tag, n2.tag, msg) self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg) self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg) @@ -24,8 +34,8 @@ class ViewCase(common.TransactionCase): # equality (!?!?!?!) self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg) - for c1, c2 in zip(n1, n2): - self.assertTreesEqual(c1, c2, msg) + for c1, c2 in itertools.izip_longest(n1, n2): + self.assertEqual(c1, c2, msg) class TestNodeLocator(common.TransactionCase): @@ -374,13 +384,6 @@ class TestApplyInheritedArchs(ViewCase): """ Applies a sequence of modificator archs to a base view """ -class TestViewCombined(ViewCase): - """ - Test fallback operations of View.read_combined: - * defaults mapping - * ? - """ - class TestNoModel(ViewCase): def test_create_view_nomodel(self): View = self.registry('ir.ui.view') @@ -628,6 +631,7 @@ class test_views(ViewCase): def _insert_view(self, **kw): """Insert view into database via a query to passtrough validation""" kw.pop('id', None) + kw.setdefault('mode', 'extension' if kw.get('inherit_id') else 'primary') keys = sorted(kw.keys()) fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys) @@ -805,6 +809,273 @@ class test_views(ViewCase): string="Replacement title", version="7.0" )) +class ViewModeField(ViewCase): + """ + This should probably, eventually, be folded back into other test case + classes, integrating the test (or not) of the mode field to regular cases + """ + + def testModeImplicitValue(self): + """ mode is auto-generated from inherit_id: + * inherit_id -> mode=extension + * not inherit_id -> mode=primary + """ + view = self.browse(self.create({ + 'inherit_id': None, + 'arch': '<qweb/>' + })) + self.assertEqual(view.mode, 'primary') + + view2 = self.browse(self.create({ + 'inherit_id': view.id, + 'arch': '<qweb/>' + })) + self.assertEqual(view2.mode, 'extension') + + @openerp.tools.mute_logger('openerp.sql_db') + def testModeExplicit(self): + view = self.browse(self.create({ + 'inherit_id': None, + 'arch': '<qweb/>' + })) + view2 = self.browse(self.create({ + 'inherit_id': view.id, + 'mode': 'primary', + 'arch': '<qweb/>' + })) + self.assertEqual(view.mode, 'primary') + + with self.assertRaises(IntegrityError): + self.create({ + 'inherit_id': None, + 'mode': 'extension', + 'arch': '<qweb/>' + }) + + @openerp.tools.mute_logger('openerp.sql_db') + def testPurePrimaryToExtension(self): + """ + A primary view with inherit_id=None can't be converted to extension + """ + view_pure_primary = self.browse(self.create({ + 'inherit_id': None, + 'arch': '<qweb/>' + })) + with self.assertRaises(IntegrityError): + view_pure_primary.write({'mode': 'extension'}) + + def testInheritPrimaryToExtension(self): + """ + A primary view with an inherit_id can be converted to extension + """ + base = self.create({'inherit_id': None, 'arch': '<qweb/>'}) + view = self.browse(self.create({ + 'inherit_id': base, + 'mode': 'primary', + 'arch': '<qweb/>' + })) + + view.write({'mode': 'extension'}) + + def testDefaultExtensionToPrimary(self): + """ + An extension view can be converted to primary + """ + base = self.create({'inherit_id': None, 'arch': '<qweb/>'}) + view = self.browse(self.create({ + 'inherit_id': base, + 'arch': '<qweb/>' + })) + + view.write({'mode': 'primary'}) + +class TestDefaultView(ViewCase): + def testDefaultViewBase(self): + self.create({ + 'inherit_id': False, + 'priority': 10, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + v2 = self.create({ + 'inherit_id': False, + 'priority': 1, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + + default = self.Views.default_view(self.cr, self.uid, False, 'qweb') + self.assertEqual( + default, v2, + "default_view should get the view with the lowest priority for " + "a (model, view_type) pair" + ) + + def testDefaultViewPrimary(self): + v1 = self.create({ + 'inherit_id': False, + 'priority': 10, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + self.create({ + 'inherit_id': False, + 'priority': 5, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + v3 = self.create({ + 'inherit_id': v1, + 'priority': 1, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + + default = self.Views.default_view(self.cr, self.uid, False, 'qweb') + self.assertEqual( + default, v3, + "default_view should get the view with the lowest priority for " + "a (model, view_type) pair in all the primary tables" + ) + +class TestViewCombined(ViewCase): + """ + * When asked for a view, instead of looking for the closest parent with + inherit_id=False look for mode=primary + * If root.inherit_id, resolve the arch for root.inherit_id (?using which + model?), then apply root's inheritance specs to it + * Apply inheriting views on top + """ + + def setUp(self): + super(TestViewCombined, self).setUp() + + self.a1 = self.create({ + 'model': 'a', + 'arch': '<qweb><a1/></qweb>' + }) + self.a2 = self.create({ + 'model': 'a', + 'inherit_id': self.a1, + 'priority': 5, + 'arch': '<xpath expr="//a1" position="after"><a2/></xpath>' + }) + self.a3 = self.create({ + 'model': 'a', + 'inherit_id': self.a1, + 'arch': '<xpath expr="//a1" position="after"><a3/></xpath>' + }) + # mode=primary should be an inheritance boundary in both direction, + # even within a model it should not extend the parent + self.a4 = self.create({ + 'model': 'a', + 'inherit_id': self.a1, + 'mode': 'primary', + 'arch': '<xpath expr="//a1" position="after"><a4/></xpath>', + }) + + self.b1 = self.create({ + 'model': 'b', + 'inherit_id': self.a3, + 'mode': 'primary', + 'arch': '<xpath expr="//a1" position="after"><b1/></xpath>' + }) + self.b2 = self.create({ + 'model': 'b', + 'inherit_id': self.b1, + 'arch': '<xpath expr="//a1" position="after"><b2/></xpath>' + }) + + self.c1 = self.create({ + 'model': 'c', + 'inherit_id': self.a1, + 'mode': 'primary', + 'arch': '<xpath expr="//a1" position="after"><c1/></xpath>' + }) + self.c2 = self.create({ + 'model': 'c', + 'inherit_id': self.c1, + 'priority': 5, + 'arch': '<xpath expr="//a1" position="after"><c2/></xpath>' + }) + self.c3 = self.create({ + 'model': 'c', + 'inherit_id': self.c2, + 'priority': 10, + 'arch': '<xpath expr="//a1" position="after"><c3/></xpath>' + }) + + self.d1 = self.create({ + 'model': 'd', + 'inherit_id': self.b1, + 'mode': 'primary', + 'arch': '<xpath expr="//a1" position="after"><d1/></xpath>' + }) + + def read_combined(self, id): + return self.Views.read_combined( + self.cr, self.uid, + id, ['arch'], + context={'check_view_ids': self.Views.search(self.cr, self.uid, [])} + ) + + def test_basic_read(self): + arch = self.read_combined(self.a1)['arch'] + self.assertEqual( + ET.fromstring(arch), + E.qweb( + E.a1(), + E.a3(), + E.a2(), + ), arch) + + def test_read_from_child(self): + arch = self.read_combined(self.a3)['arch'] + self.assertEqual( + ET.fromstring(arch), + E.qweb( + E.a1(), + E.a3(), + E.a2(), + ), arch) + + def test_read_from_child_primary(self): + arch = self.read_combined(self.a4)['arch'] + self.assertEqual( + ET.fromstring(arch), + E.qweb( + E.a1(), + E.a4(), + E.a3(), + E.a2(), + ), arch) + + def test_cross_model_simple(self): + arch = self.read_combined(self.c2)['arch'] + self.assertEqual( + ET.fromstring(arch), + E.qweb( + E.a1(), + E.c3(), + E.c2(), + E.c1(), + E.a3(), + E.a2(), + ), arch) + + def test_cross_model_double(self): + arch = self.read_combined(self.d1)['arch'] + self.assertEqual( + ET.fromstring(arch), + E.qweb( + E.a1(), + E.d1(), + E.b2(), + E.b1(), + E.a3(), + E.a2(), + ), arch) + class TestXPathExtentions(common.BaseCase): def test_hasclass(self): tree = E.node( diff --git a/openerp/import_xml.rng b/openerp/import_xml.rng index 04922268e27521371aeae0773604943ae214859e..8584267a2451e5b860b0eb10aada8e402dc446af 100644 --- a/openerp/import_xml.rng +++ b/openerp/import_xml.rng @@ -217,7 +217,14 @@ <rng:optional><rng:attribute name="priority"/></rng:optional> <rng:choice> <rng:group> - <rng:optional><rng:attribute name="inherit_id"/></rng:optional> + <rng:optional> + <rng:attribute name="inherit_id"/> + <rng:optional> + <rng:attribute name="primary"> + <rng:value>True</rng:value> + </rng:attribute> + </rng:optional> + </rng:optional> <rng:optional><rng:attribute name="inherit_option_id"/></rng:optional> <rng:optional><rng:attribute name="groups"/></rng:optional> </rng:group> diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index c84a961d2d3fb3dc811c04d61a12ed9c8e47d970..76b4d7ee3ffa93d8454db22222dca52b8e64449c 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -729,7 +729,6 @@ class BaseModel(object): _all_columns = {} _table = None - _invalids = set() _log_create = False _sql_constraints = [] _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists'] @@ -1543,9 +1542,6 @@ class BaseModel(object): yield dbid, xid, converted, dict(extras, record=stream.index) - def get_invalid_fields(self, cr, uid): - return list(self._invalids) - def _validate(self, cr, uid, ids, context=None): context = context or {} lng = context.get('lang') @@ -1566,12 +1562,9 @@ class BaseModel(object): # Check presence of __call__ directly instead of using # callable() because it will be deprecated as of Python 3.0 if hasattr(msg, '__call__'): - tmp_msg = msg(self, cr, uid, ids, context=context) - if isinstance(tmp_msg, tuple): - tmp_msg, params = tmp_msg - translated_msg = tmp_msg % params - else: - translated_msg = tmp_msg + translated_msg = msg(self, cr, uid, ids, context=context) + if isinstance(translated_msg, tuple): + translated_msg = translated_msg[0] % translated_msg[1] else: translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg) if extra_error: @@ -1579,11 +1572,8 @@ class BaseModel(object): error_msgs.append( _("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg) ) - self._invalids.update(fields) if error_msgs: raise except_orm('ValidateError', '\n'.join(error_msgs)) - else: - self._invalids.clear() def default_get(self, cr, uid, fields_list, context=None): """ diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index af6f3431229b08947e21d64e2090fa5e098d9874..8993002b1ece02ceb0ba63d72e5e9bc3ffdf9eb0 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -898,6 +898,8 @@ form: module.record_id""" % (xml_id,) record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]")) if el.attrib.pop('page', None) == 'True': record.append(Field(name="page", eval="True")) + if el.get('primary') == 'True': + record.append(Field('primary', name='mode')) return self._tag_record(cr, record, data_node)