# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import ast import json import logging import re import time from functools import partial from lxml import etree from lxml.builder import E from psycopg2 import IntegrityError from psycopg2.extras import Json from odoo.exceptions import AccessError, ValidationError from odoo.tests import common, tagged from odoo.addons.base.tests.common import TransactionCaseWithUserDemo from odoo.tools import get_cache_key_counter, mute_logger, view_validation, safe_eval from odoo.addons.base.models import ir_ui_view _logger = logging.getLogger(__name__) class ViewXMLID(common.TransactionCase): def test_model_data_id(self): """ Check whether views know their xmlid record. """ view = self.env.ref('base.view_company_form') self.assertTrue(view) self.assertTrue(view.model_data_id) self.assertEqual(view.model_data_id.complete_name, 'base.view_company_form') class ViewCase(TransactionCaseWithUserDemo): def setUp(self): super(ViewCase, self).setUp() self.View = self.env['ir.ui.view'] def assertValid(self, arch, name='valid view', inherit_id=False): return self.View.create({ 'name': name, 'model': 'ir.ui.view', 'inherit_id': inherit_id, 'arch': arch, }) def assertInvalid(self, arch, expected_message=None, name='invalid view', inherit_id=False): with mute_logger('odoo.addons.base.models.ir_ui_view'): with self.assertRaises(ValidationError) as catcher: with self.cr.savepoint(): self.View.create({ 'name': name, 'model': 'ir.ui.view', 'inherit_id': inherit_id, 'arch': arch, }) message = str(catcher.exception.args[0]) self.assertEqual(catcher.exception.context['name'], name) if expected_message: self.assertIn(expected_message, message) else: _logger.warning(message) def assertWarning(self, arch, expected_message=None, name='invalid view'): with self.assertLogs('odoo.addons.base.models.ir_ui_view', level="WARNING") as log_catcher: self.View.create({ 'name': name, 'model': 'ir.ui.view', 'arch': arch, }) self.assertEqual(len(log_catcher.output), 1, "Exactly one warning should be logged") message = log_catcher.output[0] self.assertIn('View error context', message) self.assertIn("'name': '%s'" % name, message) if expected_message: self.assertIn(expected_message, message) 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, encoding='unicode') 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 return view def get_views(self, names): return self.View.concat(*(self.view_ids[name] for name in names)) def setUp(self): super(TestViewInheritance, self).setUp() self.patch(self.registry, '_init', False) self.model = 'ir.ui.view.custom' self.view_ids = {} self.a = self.makeView("A") self.a1 = self.makeView("A1", self.a.id) self.a2 = self.makeView("A2", self.a.id) self.a11 = self.makeView("A11", self.a1.id) self.a11.mode = 'primary' self.makeView("A111", self.a11.id) self.makeView("A12", self.a1.id) self.makeView("A21", self.a2.id) self.a22 = self.makeView("A22", self.a2.id) self.makeView("A221", self.a22.id) self.b = self.makeView('B', arch=self.arch_for("B", 'tree')) self.makeView('B1', self.b.id, arch=self.arch_for("B1", 'tree', parent=self.b)) self.c = self.makeView('C', arch=self.arch_for("C", 'tree')) self.c.write({'priority': 1}) def test_get_inheriting_views(self): self.assertEqual( self.view_ids['A']._get_inheriting_views(), self.get_views('A A1 A2 A12 A21 A22 A221'.split()), ) self.assertEqual( self.view_ids['A21']._get_inheriting_views(), self.get_views(['A21']), ) self.assertEqual( self.view_ids['A11']._get_inheriting_views(), self.get_views(['A11', 'A111']), ) self.assertEqual( (self.view_ids['A11'] + self.view_ids['A'])._get_inheriting_views(), self.get_views('A A1 A2 A11 A111 A12 A21 A22 A221'.split()), ) def test_default_view(self): default = self.View.default_view(model=self.model, view_type='form') self.assertEqual(default, self.view_ids['A'].id) default_tree = self.View.default_view(model=self.model, view_type='tree') self.assertEqual(default_tree, self.view_ids['C'].id) def test_no_default_view(self): self.assertFalse(self.View.default_view(model='no_model.exist', view_type='form')) self.assertFalse(self.View.default_view(model=self.model, view_type='graph')) def test_no_recursion(self): r1 = self.makeView('R1') with self.assertRaises(ValidationError), self.cr.savepoint(): r1.write({'inherit_id': r1.id}) r2 = self.makeView('R2', r1.id) r3 = self.makeView('R3', r2.id) with self.assertRaises(ValidationError), self.cr.savepoint(): r2.write({'inherit_id': r3.id}) with self.assertRaises(ValidationError), self.cr.savepoint(): r1.write({'inherit_id': r3.id}) with self.assertRaises(ValidationError), self.cr.savepoint(): r1.write({ 'inherit_id': r1.id, 'arch': self.arch_for('itself', parent=True), }) def test_write_arch(self): self.env['res.lang']._activate_lang('fr_FR') v = self.makeView("T", arch='
Bar
') v.update_field_translations('arch_db', {'fr_FR': {'Foo': 'Fou', 'Bar': 'Barre'}}) self.assertEqual(v.arch, '
Bar
') # modify v to discard translations; this should not invalidate 'arch'! v.arch = '
' self.assertEqual(v.arch, '') def test_get_combined_arch_query_count(self): # If the query count increases, you probably made the view combination # fetch an extra field on views. You better fetch that extra field with # the query of _get_inheriting_views() and manually feed the cache. self.env.invalidate_all() with self.assertQueryCount(3): # 1: browse([self.view_ids['A']]) # 2: _get_inheriting_views: id, inherit_id, mode, groups # 3: _combine: arch_db self.view_ids['A'].get_combined_arch() def test_view_validate_button_action_query_count(self): _, _, counter = get_cache_key_counter(self.env['ir.model.data']._xmlid_lookup, 'base.action_ui_view') hit, miss = counter.hit, counter.miss with self.assertQueryCount(7): base_view = self.assertValid("""
""") self.assertEqual(counter.hit, hit) self.assertEqual(counter.miss, miss + 2) with self.assertQueryCount(6): self.assertValid(""" """, inherit_id=base_view.id) self.assertEqual(counter.hit, hit + 2) self.assertEqual(counter.miss, miss + 2) def test_view_validate_attrs_groups_query_count(self): _, _, counter = get_cache_key_counter(self.env['ir.model.data']._xmlid_lookup, 'base.group_system') hit, miss = counter.hit, counter.miss with self.assertQueryCount(4): base_view = self.assertValid("""
""") self.assertEqual(counter.hit, hit) self.assertEqual(counter.miss, miss + 1) with self.assertQueryCount(4): self.assertValid(""" """, inherit_id=base_view.id) self.assertEqual(counter.hit, hit + 1) self.assertEqual(counter.miss, miss + 1) 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") self.adv_arch = E.form( E.field( "TEXT1", E.field(name="subtarget"), "TEXT2", E.field(name="anothersubtarget"), "TEXT3", name="target", ), string="Title") def test_replace_outer(self): spec = E.field( E.field(name="replacement"), name="target", position="replace") self.View.apply_inheritance_specs(self.base_arch, spec) 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) 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) 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) 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) self.View.apply_inheritance_specs(self.base_arch, spec) self.assertEqual( self.base_arch, E.form( E.field( E.field(name="inserted"), E.field(name="inserted 2"), name="target"), string="Title")) def test_replace_inner(self): spec = E.field( "TEXT 4", E.field(name="replacement"), "TEXT 5", E.field(name="replacement2"), "TEXT 6", name="target", position="replace", mode="inner") expected = E.form( E.field( "TEXT 4", E.field(name="replacement"), "TEXT 5", E.field(name="replacement2"), "TEXT 6", name="target"), string="Title") # applying spec to both base_arch and adv_arch is expected to give the same result self.View.apply_inheritance_specs(self.base_arch, spec) self.assertEqual(self.base_arch, expected) self.View.apply_inheritance_specs(self.adv_arch, spec) self.assertEqual(self.adv_arch, expected) 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) 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('odoo.addons.base.models.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) @mute_logger('odoo.addons.base.models.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) @mute_logger('odoo.addons.base.models.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) 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) 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 TestApplyInheritanceMoveSpecs(ViewCase): def setUp(self): super(TestApplyInheritanceMoveSpecs, self).setUp() self.base_arch = E.template( E.div(E.p("Content", {'class': 'some'})), E.div({'class': 'target'}) ) self.wrapped_arch = E.template( E.div("aaaa", E.p("Content", {'class': 'some'}), "bbbb"), E.div({'class': 'target'}) ) def apply_spec(self, arch, spec): self.View.apply_inheritance_specs(arch, spec) def test_move_replace(self): spec = E.xpath( E.xpath(expr="//p", position="move"), expr="//div[@class='target']", position="replace") self.apply_spec(self.base_arch, spec) self.assertEqual( self.base_arch, E.template( E.div(), E.p("Content", {'class': 'some'}) ) ) self.apply_spec(self.wrapped_arch, spec) self.assertEqual( self.wrapped_arch, E.template( E.div("aaaabbbb"), E.p("Content", {'class': 'some'}) ) ) def test_move_inside(self): spec = E.xpath( E.xpath(expr="//p", position="move"), expr="//div[@class='target']", position="inside") self.apply_spec(self.base_arch, spec) self.assertEqual( self.base_arch, E.template( E.div(), E.div(E.p("Content", {'class': 'some'}), {'class': 'target'}) ) ) self.apply_spec(self.wrapped_arch, spec) self.assertEqual( self.wrapped_arch, E.template( E.div("aaaabbbb"), E.div(E.p("Content", {'class': 'some'}), {'class': 'target'}) ) ) def test_move_before(self): spec = E.xpath( E.xpath(expr="//p", position="move"), expr="//div[@class='target']", position="before") self.apply_spec(self.base_arch, spec) self.assertEqual( self.base_arch, E.template( E.div(""), E.p("Content", {'class': 'some'}), E.div({'class': 'target'}), ) ) self.apply_spec(self.wrapped_arch, spec) self.assertEqual( self.wrapped_arch, E.template( E.div("aaaabbbb"), E.p("Content", {'class': 'some'}), E.div({'class': 'target'}), ) ) def test_move_after(self): spec = E.xpath( E.xpath(expr="//p", position="move"), expr="//div[@class='target']", position="after") self.apply_spec(self.base_arch, spec) self.assertEqual( self.base_arch, E.template( E.div(), E.div({'class': 'target'}), E.p("Content", {'class': 'some'}), ) ) self.apply_spec(self.wrapped_arch, spec) self.assertEqual( self.wrapped_arch, E.template( E.div("aaaabbbb"), E.div({'class': 'target'}), E.p("Content", {'class': 'some'}), ) ) def test_move_with_other_1(self): # multiple elements with move in first position spec = E.xpath( E.xpath(expr="//p", position="move"), E.p("Content2", {'class': 'new_p'}), expr="//div[@class='target']", position="after") self.apply_spec(self.base_arch, spec) self.assertEqual( self.base_arch, E.template( E.div(), E.div({'class': 'target'}), E.p("Content", {'class': 'some'}), E.p("Content2", {'class': 'new_p'}), ) ) def test_move_with_other_2(self): # multiple elements with move in last position spec = E.xpath( E.p("Content2", {'class': 'new_p'}), E.xpath(expr="//p", position="move"), expr="//div[@class='target']", position="after") self.apply_spec(self.wrapped_arch, spec) self.assertEqual( self.wrapped_arch, E.template( E.div("aaaabbbb"), E.div({'class': 'target'}), E.p("Content2", {'class': 'new_p'}), E.p("Content", {'class': 'some'}), ) ) @mute_logger('odoo.addons.base.models.ir_ui_view') def test_incorrect_move_1(self): # cannot move an inexisting element spec = E.xpath( E.xpath(expr="//p[@name='none']", position="move"), expr="//div[@class='target']", position="after") with self.assertRaises(ValueError): self.apply_spec(self.base_arch, spec) @mute_logger('odoo.addons.base.models.ir_ui_view') def test_incorrect_move_2(self): # move xpath cannot contain any children spec = E.xpath( E.xpath(E.p("Content2", {'class': 'new_p'}), expr="//p", position="move"), expr="//div[@class='target']", position="after") with self.assertRaises(ValueError): self.apply_spec(self.base_arch, spec) def test_incorrect_move_3(self): # move won't be correctly applied if not a direct child of an xpath spec = E.xpath( E.div(E.xpath(E.p("Content2", {'class': 'new_p'}), expr="//p", position="move"), {'class': 'wrapper'}), expr="//div[@class='target']", position="after") self.apply_spec(self.base_arch, spec) self.assertEqual( self.base_arch, E.template( E.div(E.p("Content", {'class': 'some'})), E.div({'class': 'target'}), E.div(E.xpath(E.p("Content2", {'class': 'new_p'}), expr="//p", position="move"), {'class': 'wrapper'}), ) ) 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': '