diff options
Diffstat (limited to 'odoo/addons/base/tests/test_views.py')
| -rw-r--r-- | odoo/addons/base/tests/test_views.py | 1177 |
1 files changed, 1177 insertions, 0 deletions
diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py new file mode 100644 index 00000000000..1ece193f26a --- /dev/null +++ b/odoo/addons/base/tests/test_views.py @@ -0,0 +1,1177 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from functools import partial +from itertools import izip_longest + +from lxml import etree +from lxml.builder import E +from psycopg2 import IntegrityError + +from odoo.tests import common +from odoo.tools import mute_logger + + +class ViewCase(common.TransactionCase): + def setUp(self): + super(ViewCase, self).setUp() + self.addTypeEqualityFunc(etree._Element, self.assertTreesEqual) + self.View = self.env['ir.ui.view'] + + def assertTreesEqual(self, n1, n2, msg=None): + 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) + + # Because lxml uses ordereddicts in which order is important to + # equality (!?!?!?!) + self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg) + + for c1, c2 in izip_longest(n1, n2): + self.assertEqual(c1, c2, msg) + + +class TestNodeLocator(common.TransactionCase): + """ + The node locator returns None when it can not find a node, and the first + match when it finds something (no jquery-style node sets) + """ + + def test_no_match_xpath(self): + """ + xpath simply uses the provided @expr pattern to find a node + """ + node = self.env['ir.ui.view'].locate_node( + E.root(E.foo(), E.bar(), E.baz()), + E.xpath(expr="//qux"), + ) + self.assertIsNone(node) + + def test_match_xpath(self): + bar = E.bar() + node = self.env['ir.ui.view'].locate_node( + E.root(E.foo(), bar, E.baz()), + E.xpath(expr="//bar"), + ) + self.assertIs(node, bar) + + def test_no_match_field(self): + """ + A field spec will match by @name against all fields of the view + """ + node = self.env['ir.ui.view'].locate_node( + E.root(E.foo(), E.bar(), E.baz()), + E.field(name="qux"), + ) + self.assertIsNone(node) + + node = self.env['ir.ui.view'].locate_node( + E.root(E.field(name="foo"), E.field(name="bar"), E.field(name="baz")), + E.field(name="qux"), + ) + self.assertIsNone(node) + + def test_match_field(self): + bar = E.field(name="bar") + node = self.env['ir.ui.view'].locate_node( + E.root(E.field(name="foo"), bar, E.field(name="baz")), + E.field(name="bar"), + ) + self.assertIs(node, bar) + + def test_no_match_other(self): + """ + Non-xpath non-fields are matched by node name first + """ + node = self.env['ir.ui.view'].locate_node( + E.root(E.foo(), E.bar(), E.baz()), + E.qux(), + ) + self.assertIsNone(node) + + def test_match_other(self): + bar = E.bar() + node = self.env['ir.ui.view'].locate_node( + E.root(E.foo(), bar, E.baz()), + E.bar(), + ) + self.assertIs(bar, node) + + def test_attribute_mismatch(self): + """ + Non-xpath non-field are filtered by matching attributes on spec and + matched nodes + """ + node = self.env['ir.ui.view'].locate_node( + E.root(E.foo(attr='1'), E.bar(attr='2'), E.baz(attr='3')), + E.bar(attr='5'), + ) + self.assertIsNone(node) + + def test_attribute_filter(self): + match = E.bar(attr='2') + node = self.env['ir.ui.view'].locate_node( + E.root(E.bar(attr='1'), match, E.root(E.bar(attr='3'))), + E.bar(attr='2'), + ) + self.assertIs(node, match) + + def test_version_mismatch(self): + """ + A @version on the spec will be matched against the view's version + """ + node = self.env['ir.ui.view'].locate_node( + E.root(E.foo(attr='1'), version='4'), + E.foo(attr='1', version='3'), + ) + self.assertIsNone(node) + + +class TestViewInheritance(ViewCase): + def arch_for(self, name, view_type='form', parent=None): + """ Generates a trivial view of the specified ``view_type``. + + The generated view is empty but ``name`` is set as its root's ``@string``. + + If ``parent`` is not falsy, generates an extension view (instead of + a root view) replacing the parent's ``@string`` by ``name`` + + :param str name: ``@string`` value for the view root + :param str view_type: + :param bool parent: + :return: generated arch + :rtype: str + """ + if not parent: + element = E(view_type, string=name) + else: + element = E(view_type, + E.attribute(name, name='string'), + position='attributes' + ) + return etree.tostring(element) + + def makeView(self, name, parent=None, arch=None): + """ Generates a basic ir.ui.view with the provided name, parent and arch. + + If no parent is provided, the view is top-level. + + If no arch is provided, generates one by calling :meth:`~.arch_for`. + + :param str name: + :param int parent: id of the parent view, if any + :param str arch: + :returns: the created view's id. + :rtype: int + """ + view = self.View.create({ + 'model': self.model, + 'name': name, + 'arch': arch or self.arch_for(name, parent=parent), + 'inherit_id': parent, + 'priority': 5, # higher than default views + }) + self.view_ids[name] = view.id + return view + + def setUp(self): + super(TestViewInheritance, self).setUp() + + self.patch(self.registry, '_init', False) + + self.model = 'ir.ui.view.custom' + self.view_ids = {} + + a = self.makeView("A") + a1 = self.makeView("A1", a.id) + a11 = self.makeView("A11", a1.id) + self.makeView("A111", a11.id) + self.makeView("A12", a1.id) + a2 = self.makeView("A2", a.id) + self.makeView("A21", a2.id) + a22 = self.makeView("A22", a2.id) + self.makeView("A221", a22.id) + + b = self.makeView('B', arch=self.arch_for("B", 'tree')) + self.makeView('B1', b.id, arch=self.arch_for("B1", 'tree', parent=b)) + c = self.makeView('C', arch=self.arch_for("C", 'tree')) + c.write({'priority': 1}) + + def test_get_inheriting_views_arch(self): + self.assertEqual( + self.View.get_inheriting_views_arch(self.view_ids['A'], self.model), [ + (self.arch_for('A1', parent=True), self.view_ids['A1']), + (self.arch_for('A2', parent=True), self.view_ids['A2']), + ]) + + self.assertEqual( + self.View.get_inheriting_views_arch(self.view_ids['A21'], self.model), + []) + + self.assertEqual( + self.View.get_inheriting_views_arch(self.view_ids['A11'], self.model), + [(self.arch_for('A111', parent=True), self.view_ids['A111'])]) + + def test_default_view(self): + default = self.View.default_view(model=self.model, view_type='form') + self.assertEqual(default, self.view_ids['A']) + + default_tree = self.View.default_view(model=self.model, view_type='tree') + self.assertEqual(default_tree, self.view_ids['C']) + + def test_no_default_view(self): + self.assertFalse(self.View.default_view(model='does.not.exist', view_type='form')) + self.assertFalse(self.View.default_view(model=self.model, view_type='graph')) + + +class TestApplyInheritanceSpecs(ViewCase): + """ Applies a sequence of inheritance specification nodes to a base + architecture. IO state parameters (cr, uid, model, context) are used for + error reporting + + The base architecture is altered in-place. + """ + def setUp(self): + super(TestApplyInheritanceSpecs, self).setUp() + self.base_arch = E.form( + E.field(name="target"), + string="Title") + + def test_replace(self): + spec = E.field( + E.field(name="replacement"), + name="target", position="replace") + + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + self.assertEqual( + self.base_arch, + E.form(E.field(name="replacement"), string="Title")) + + def test_delete(self): + spec = E.field(name="target", position="replace") + + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + self.assertEqual( + self.base_arch, + E.form(string="Title")) + + def test_insert_after(self): + spec = E.field( + E.field(name="inserted"), + name="target", position="after") + + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + self.assertEqual( + self.base_arch, + E.form( + E.field(name="target"), + E.field(name="inserted"), + string="Title" + )) + + def test_insert_before(self): + spec = E.field( + E.field(name="inserted"), + name="target", position="before") + + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + self.assertEqual( + self.base_arch, + E.form( + E.field(name="inserted"), + E.field(name="target"), + string="Title")) + + def test_insert_inside(self): + default = E.field(E.field(name="inserted"), name="target") + spec = E.field(E.field(name="inserted 2"), name="target", position='inside') + + self.View.apply_inheritance_specs(self.base_arch, default, None) + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + self.assertEqual( + self.base_arch, + E.form( + E.field( + E.field(name="inserted"), + E.field(name="inserted 2"), + name="target"), + string="Title")) + + def test_unpack_data(self): + spec = E.data( + E.field(E.field(name="inserted 0"), name="target"), + E.field(E.field(name="inserted 1"), name="target"), + E.field(E.field(name="inserted 2"), name="target"), + E.field(E.field(name="inserted 3"), name="target"), + ) + + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + self.assertEqual( + self.base_arch, + E.form( + E.field( + E.field(name="inserted 0"), + E.field(name="inserted 1"), + E.field(name="inserted 2"), + E.field(name="inserted 3"), + name="target"), + string="Title")) + + @mute_logger('openerp.addons.base.ir.ir_ui_view') + def test_invalid_position(self): + spec = E.field( + E.field(name="whoops"), + name="target", position="serious_series") + + with self.assertRaises(ValueError): + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + @mute_logger('openerp.addons.base.ir.ir_ui_view') + def test_incorrect_version(self): + # Version ignored on //field elements, so use something else + arch = E.form(E.element(foo="42")) + spec = E.element( + E.field(name="placeholder"), + foo="42", version="7.0") + + with self.assertRaises(ValueError): + self.View.apply_inheritance_specs(arch, spec, None) + + @mute_logger('openerp.addons.base.ir.ir_ui_view') + def test_target_not_found(self): + spec = E.field(name="targut") + + with self.assertRaises(ValueError): + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + +class TestApplyInheritanceWrapSpecs(ViewCase): + def setUp(self): + super(TestApplyInheritanceWrapSpecs, self).setUp() + self.base_arch = E.template(E.div(E.p("Content"))) + + def apply_spec(self, spec): + self.View.apply_inheritance_specs(self.base_arch, spec, None) + + def test_replace(self): + spec = E.xpath( + E.div("$0", {'class': "some"}), + expr="//p", position="replace") + + self.apply_spec(spec) + self.assertEqual( + self.base_arch, + E.template(E.div( + E.div(E.p('Content'), {'class': 'some'}) + )) + ) + +class TestApplyInheritedArchs(ViewCase): + """ Applies a sequence of modificator archs to a base view + """ + + +class TestNoModel(ViewCase): + def test_create_view_nomodel(self): + view = self.View.create({ + 'name': 'dummy', + 'arch': '<template name="foo"/>', + 'inherit_id': False, + 'type': 'qweb', + }) + fields = ['name', 'arch', 'type', 'priority', 'inherit_id', 'model'] + [data] = view.read(fields) + self.assertEqual(data, { + 'id': view.id, + 'name': 'dummy', + 'arch': '<template name="foo"/>', + 'type': 'qweb', + 'priority': 16, + 'inherit_id': False, + 'model': False, + }) + + text_para = E.p("", {'class': 'legalese'}) + arch = E.body( + E.div( + E.h1("Title"), + id="header"), + E.p("Welcome!"), + E.div( + E.hr(), + text_para, + id="footer"), + {'class': "index"},) + + def test_qweb_translation(self): + """ + Test if translations work correctly without a model + """ + self.env['res.lang'].load_lang('fr_FR') + ARCH = '<template name="foo">%s</template>' + TEXT_EN = "Copyright copyrighter" + TEXT_FR = u"Copyrighter, tous droits réservés" + view = self.View.create({ + 'name': 'dummy', + 'arch': ARCH % TEXT_EN, + 'inherit_id': False, + 'type': 'qweb', + }) + self.env['ir.translation'].create({ + 'type': 'model', + 'name': 'ir.ui.view,arch_db', + 'res_id': view.id, + 'lang': 'fr_FR', + 'src': TEXT_EN, + 'value': TEXT_FR, + }) + view = view.with_context(lang='fr_FR') + self.assertEqual(view.arch, ARCH % TEXT_FR) + + +class TestTemplating(ViewCase): + def setUp(self): + super(TestTemplating, self).setUp() + self.patch(self.registry, '_init', False) + + def test_branding_inherit(self): + view1 = self.View.create({ + 'name': "Base view", + 'type': 'qweb', + 'arch': """<root> + <item order="1"/> + </root> + """ + }) + view2 = self.View.create({ + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': view1.id, + 'arch': """<xpath expr="//item" position="before"> + <item order="2"/> + </xpath> + """ + }) + + arch_string = view1.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + [initial] = arch.xpath('//item[@order=1]') + self.assertEqual( + str(view1.id), + initial.get('data-oe-id'), + "initial should come from the root view") + self.assertEqual( + '/root[1]/item[1]', + initial.get('data-oe-xpath'), + "initial's xpath should be within the root view only") + + [second] = arch.xpath('//item[@order=2]') + self.assertEqual( + str(view2.id), + second.get('data-oe-id'), + "second should come from the extension view") + + def test_branding_distribute_inner(self): + """ Checks that the branding is correctly distributed within a view + extension + """ + view1 = self.View.create({ + 'name': "Base view", + 'type': 'qweb', + 'arch': """<root> + <item order="1"/> + </root>""" + }) + view2 = self.View.create({ + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': view1.id, + 'arch': """<xpath expr="//item" position="before"> + <item order="2"> + <content t-att-href="foo">bar</content> + </item> + </xpath>""" + }) + + arch_string = view1.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + self.assertEqual( + arch, + E.root( + E.item( + E.content("bar", { + 't-att-href': "foo", + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(view2.id), + 'data-oe-field': 'arch', + 'data-oe-xpath': '/xpath/item/content[1]', + }), { + 'order': '2', + }), + E.item({ + 'order': '1', + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(view1.id), + 'data-oe-field': 'arch', + 'data-oe-xpath': '/root[1]/item[1]', + }) + ) + ) + + def test_esc_no_branding(self): + view = self.View.create({ + 'name': "Base View", + 'type': 'qweb', + 'arch': """<root> + <item><span t-esc="foo"/></item> + </root>""", + }) + + arch_string = view.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + self.assertEqual(arch, E.root(E.item(E.span({'t-esc': "foo"})))) + + def test_ignore_unbrand(self): + view1 = self.View.create({ + 'name': "Base view", + 'type': 'qweb', + 'arch': """<root> + <item order="1" t-ignore="true"> + <t t-esc="foo"/> + </item> + </root>""" + }) + view2 = self.View.create({ + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': view1.id, + 'arch': """<xpath expr="//item[@order='1']" position="inside"> + <item order="2"> + <content t-att-href="foo">bar</content> + </item> + </xpath>""" + }) + + arch_string = view1.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + self.assertEqual( + arch, + E.root( + E.item( + {'t-ignore': 'true', 'order': '1'}, + E.t({'t-esc': 'foo'}), + E.item( + {'order': '2'}, + E.content( + {'t-att-href': 'foo'}, + "bar") + ) + ) + ), + "t-ignore should apply to injected sub-view branding, not just to" + " the main view's" + ) + + +class TestViews(ViewCase): + + def test_nonexistent_attribute_removal(self): + self.View.create({ + 'name': 'Test View', + 'model': 'ir.ui.view', + 'inherit_id': self.ref('base.view_view_tree'), + 'arch': """<?xml version="1.0"?> + <xpath expr="//field[@name='name']" position="attributes"> + <attribute name="non_existing_attribute"></attribute> + </xpath> + """, + }) + + 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') + kw.setdefault('active', True) + + keys = sorted(kw.keys()) + fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys) + params = ','.join('%%(%s)s' % (k,) for k in keys) + + query = 'INSERT INTO ir_ui_view(%s) VALUES(%s) RETURNING id' % (fields, params) + self.cr.execute(query, kw) + return self.cr.fetchone()[0] + + def test_custom_view_validation(self): + model = 'ir.actions.act_url' + validate = partial(self.View._validate_custom_views, model) + + # validation of a single view + vid = self._insert_view( + name='base view', + model=model, + priority=1, + arch_db="""<?xml version="1.0"?> + <tree string="view"> + <field name="url"/> + </tree> + """, + ) + self.assertTrue(validate()) # single view + + # validation of a inherited view + self._insert_view( + name='inherited view', + model=model, + priority=1, + inherit_id=vid, + arch_db="""<?xml version="1.0"?> + <xpath expr="//field[@name='url']" position="before"> + <field name="name"/> + </xpath> + """, + ) + self.assertTrue(validate()) # inherited view + + # validation of a second inherited view (depending on 1st) + self._insert_view( + name='inherited view 2', + model=model, + priority=5, + inherit_id=vid, + arch_db="""<?xml version="1.0"?> + <xpath expr="//field[@name='name']" position="after"> + <field name="target"/> + </xpath> + """, + ) + self.assertTrue(validate()) # inherited view + + def test_view_inheritance(self): + view1 = self.View.create({ + 'name': "bob", + 'model': 'ir.ui.view', + 'arch': """ + <form string="Base title" version="7.0"> + <separator name="separator" string="Separator" colspan="4"/> + <footer> + <button name="action_next" type="object" string="Next button" class="btn-primary"/> + <button string="Skip" special="cancel" class="btn-default"/> + </footer> + </form> + """ + }) + view2 = self.View.create({ + 'name': "edmund", + 'model': 'ir.ui.view', + 'inherit_id': view1.id, + 'arch': """ + <data> + <form position="attributes" version="7.0"> + <attribute name="string">Replacement title</attribute> + </form> + <footer position="replace"> + <footer> + <button name="action_next" type="object" string="New button"/> + </footer> + </footer> + <separator name="separator" position="replace"> + <p>Replacement data</p> + </separator> + </data> + """ + }) + view3 = self.View.create({ + 'name': 'jake', + 'model': 'ir.ui.view', + 'inherit_id': view1.id, + 'priority': 17, + 'arch': """ + <footer position="attributes"> + <attribute name="thing">bob tata lolo</attribute> + <attribute name="thing" add="bibi and co" remove="tata" separator=" " /> + <attribute name="otherthing">bob, tata,lolo</attribute> + <attribute name="otherthing" remove="tata, bob"/> + </footer> + """ + }) + + view = self.View.with_context(check_view_ids=[view2.id, view3.id]) \ + .fields_view_get(view2.id, view_type='form') + self.assertEqual(view['type'], 'form') + self.assertEqual( + etree.fromstring( + view['arch'], + parser=etree.XMLParser(remove_blank_text=True) + ), + E.form( + E.p("Replacement data"), + E.footer( + E.button(name="action_next", type="object", string="New button"), + thing="bob lolo bibi and co", otherthing="lolo" + ), + string="Replacement title", version="7.0")) + + def test_view_inheritance_divergent_models(self): + view1 = self.View.create({ + 'name': "bob", + 'model': 'ir.ui.view.custom', + 'arch': """ + <form string="Base title" version="7.0"> + <separator name="separator" string="Separator" colspan="4"/> + <footer> + <button name="action_next" type="object" string="Next button" class="btn-primary"/> + <button string="Skip" special="cancel" class="btn-default"/> + </footer> + </form> + """ + }) + view2 = self.View.create({ + 'name': "edmund", + 'model': 'ir.ui.view', + 'inherit_id': view1.id, + 'arch': """ + <data> + <form position="attributes" version="7.0"> + <attribute name="string">Replacement title</attribute> + </form> + <footer position="replace"> + <footer> + <button name="action_next" type="object" string="New button"/> + </footer> + </footer> + <separator name="separator" position="replace"> + <p>Replacement data</p> + </separator> + </data> + """ + }) + view3 = self.View.create({ + 'name': 'jake', + 'model': 'ir.ui.menu', + 'inherit_id': view1.id, + 'priority': 17, + 'arch': """ + <footer position="attributes"> + <attribute name="thing">bob</attribute> + </footer> + """ + }) + + view = self.View.with_context(check_view_ids=[view2.id, view3.id]) \ + .fields_view_get(view2.id, view_type='form') + self.assertEqual(view['type'], 'form') + self.assertEqual( + etree.fromstring( + view['arch'], + parser=etree.XMLParser(remove_blank_text=True) + ), + E.form( + E.p("Replacement data"), + E.footer( + E.button(name="action_next", type="object", string="New button")), + 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.View.create({ + 'inherit_id': None, + 'arch': '<qweb/>' + }) + self.assertEqual(view.mode, 'primary') + + view2 = self.View.create({ + 'inherit_id': view.id, + 'arch': '<qweb/>' + }) + self.assertEqual(view2.mode, 'extension') + + @mute_logger('openerp.sql_db') + def testModeExplicit(self): + view = self.View.create({ + 'inherit_id': None, + 'arch': '<qweb/>' + }) + view2 = self.View.create({ + 'inherit_id': view.id, + 'mode': 'primary', + 'arch': '<qweb/>' + }) + self.assertEqual(view.mode, 'primary') + + with self.assertRaises(IntegrityError): + self.View.create({ + 'inherit_id': None, + 'mode': 'extension', + 'arch': '<qweb/>' + }) + + @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.View.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.View.create({ + 'inherit_id': None, + 'arch': '<qweb/>', + }) + view = self.View.create({ + 'inherit_id': base.id, + 'mode': 'primary', + 'arch': '<qweb/>' + }) + + view.write({'mode': 'extension'}) + + def testDefaultExtensionToPrimary(self): + """ + An extension view can be converted to primary + """ + base = self.View.create({ + 'inherit_id': None, + 'arch': '<qweb/>', + }) + view = self.View.create({ + 'inherit_id': base.id, + 'arch': '<qweb/>' + }) + + view.write({'mode': 'primary'}) + + +class TestDefaultView(ViewCase): + def testDefaultViewBase(self): + self.View.create({ + 'inherit_id': False, + 'priority': 10, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + view2 = self.View.create({ + 'inherit_id': False, + 'priority': 1, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + + default = self.View.default_view(False, 'qweb') + self.assertEqual( + default, view2.id, + "default_view should get the view with the lowest priority for " + "a (model, view_type) pair" + ) + + def testDefaultViewPrimary(self): + view1 = self.View.create({ + 'inherit_id': False, + 'priority': 10, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + self.View.create({ + 'inherit_id': False, + 'priority': 5, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + view3 = self.View.create({ + 'inherit_id': view1.id, + 'priority': 1, + 'mode': 'primary', + 'arch': '<qweb/>', + }) + + default = self.View.default_view(False, 'qweb') + self.assertEqual( + default, view3.id, + "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.View.create({ + 'model': 'a', + 'arch': '<qweb><a1/></qweb>' + }) + self.a2 = self.View.create({ + 'model': 'a', + 'inherit_id': self.a1.id, + 'priority': 5, + 'arch': '<xpath expr="//a1" position="after"><a2/></xpath>' + }) + self.a3 = self.View.create({ + 'model': 'a', + 'inherit_id': self.a1.id, + '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.View.create({ + 'model': 'a', + 'inherit_id': self.a1.id, + 'mode': 'primary', + 'arch': '<xpath expr="//a1" position="after"><a4/></xpath>', + }) + + self.b1 = self.View.create({ + 'model': 'b', + 'inherit_id': self.a3.id, + 'mode': 'primary', + 'arch': '<xpath expr="//a1" position="after"><b1/></xpath>' + }) + self.b2 = self.View.create({ + 'model': 'b', + 'inherit_id': self.b1.id, + 'arch': '<xpath expr="//a1" position="after"><b2/></xpath>' + }) + + self.c1 = self.View.create({ + 'model': 'c', + 'inherit_id': self.a1.id, + 'mode': 'primary', + 'arch': '<xpath expr="//a1" position="after"><c1/></xpath>' + }) + self.c2 = self.View.create({ + 'model': 'c', + 'inherit_id': self.c1.id, + 'priority': 5, + 'arch': '<xpath expr="//a1" position="after"><c2/></xpath>' + }) + self.c3 = self.View.create({ + 'model': 'c', + 'inherit_id': self.c2.id, + 'priority': 10, + 'arch': '<xpath expr="//a1" position="after"><c3/></xpath>' + }) + + self.d1 = self.View.create({ + 'model': 'd', + 'inherit_id': self.b1.id, + 'mode': 'primary', + 'arch': '<xpath expr="//a1" position="after"><d1/></xpath>' + }) + + def test_basic_read(self): + context = {'check_view_ids': self.View.search([]).ids} + arch = self.a1.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.a1(), + E.a3(), + E.a2(), + ), arch) + + def test_read_from_child(self): + context = {'check_view_ids': self.View.search([]).ids} + arch = self.a3.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.a1(), + E.a3(), + E.a2(), + ), arch) + + def test_read_from_child_primary(self): + context = {'check_view_ids': self.View.search([]).ids} + arch = self.a4.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.a1(), + E.a4(), + E.a3(), + E.a2(), + ), arch) + + def test_cross_model_simple(self): + context = {'check_view_ids': self.View.search([]).ids} + arch = self.c2.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.a1(), + E.c3(), + E.c2(), + E.c1(), + E.a3(), + E.a2(), + ), arch) + + def test_cross_model_double(self): + context = {'check_view_ids': self.View.search([]).ids} + arch = self.d1.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.a1(), + E.d1(), + E.b2(), + E.b1(), + E.a3(), + E.a2(), + ), arch) + + +class TestOptionalViews(ViewCase): + """ + Tests ability to enable/disable inherited views, formerly known as + inherit_option_id + """ + + def setUp(self): + super(TestOptionalViews, self).setUp() + self.v0 = self.View.create({ + 'model': 'a', + 'arch': '<qweb><base/></qweb>', + }) + self.v1 = self.View.create({ + 'model': 'a', + 'inherit_id': self.v0.id, + 'active': True, + 'priority': 10, + 'arch': '<xpath expr="//base" position="after"><v1/></xpath>', + }) + self.v2 = self.View.create({ + 'model': 'a', + 'inherit_id': self.v0.id, + 'active': True, + 'priority': 9, + 'arch': '<xpath expr="//base" position="after"><v2/></xpath>', + }) + self.v3 = self.View.create({ + 'model': 'a', + 'inherit_id': self.v0.id, + 'active': False, + 'priority': 8, + 'arch': '<xpath expr="//base" position="after"><v3/></xpath>' + }) + + def test_applied(self): + """ mandatory and enabled views should be applied + """ + context = {'check_view_ids': self.View.search([]).ids} + arch = self.v0.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.base(), + E.v1(), + E.v2(), + ) + ) + + def test_applied_state_toggle(self): + """ Change active states of v2 and v3, check that the results + are as expected + """ + self.v2.toggle() + context = {'check_view_ids': self.View.search([]).ids} + arch = self.v0.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.base(), + E.v1(), + ) + ) + + self.v3.toggle() + context = {'check_view_ids': self.View.search([]).ids} + arch = self.v0.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.base(), + E.v1(), + E.v3(), + ) + ) + + self.v2.toggle() + context = {'check_view_ids': self.View.search([]).ids} + arch = self.v0.with_context(context).read_combined(['arch'])['arch'] + self.assertEqual( + etree.fromstring(arch), + E.qweb( + E.base(), + E.v1(), + E.v2(), + E.v3(), + ) + ) + + +class TestXPathExtentions(common.BaseCase): + def test_hasclass(self): + tree = E.node( + E.node({'class': 'foo bar baz'}), + E.node({'class': 'foo bar'}), + {'class': "foo"}) + + self.assertEqual( + len(tree.xpath('//node[hasclass("foo")]')), + 3) + self.assertEqual( + len(tree.xpath('//node[hasclass("bar")]')), + 2) + self.assertEqual( + len(tree.xpath('//node[hasclass("baz")]')), + 1) + self.assertEqual( + len(tree.xpath('//node[hasclass("foo")][not(hasclass("bar"))]')), + 1) + self.assertEqual( + len(tree.xpath('//node[hasclass("foo", "baz")]')), + 1) |
