odoo_17.0.1/odoo/addons/base/tests/test_views.py

4784 lines
184 KiB
Python
Raw Permalink Normal View History

# -*- 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='<form string="Foo">Bar</form>')
v.update_field_translations('arch_db', {'fr_FR': {'Foo': 'Fou', 'Bar': 'Barre'}})
self.assertEqual(v.arch, '<form string="Foo">Bar</form>')
# modify v to discard translations; this should not invalidate 'arch'!
v.arch = '<form/>'
self.assertEqual(v.arch, '<form/>')
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("""
<form string="View">
<header>
<button type="action" name="base.action_ui_view"/>
<button type="action" name="base.action_ui_view_custom"/>
<button type="action" name="base.action_ui_view"/>
</header>
<field name="name"/>
</form>
""")
self.assertEqual(counter.hit, hit)
self.assertEqual(counter.miss, miss + 2)
with self.assertQueryCount(6):
self.assertValid("""
<field name="name" position="replace"/>
""", 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("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="priority" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system"/>
</form>
""")
self.assertEqual(counter.hit, hit)
self.assertEqual(counter.miss, miss + 1)
with self.assertQueryCount(4):
self.assertValid("""
<field name="name" position="replace">
<field name="key" groups="base.group_system"/>
</field>
""", 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': '<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']._activate_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',
})
view.update_field_translations('arch_db', {'fr_FR': {TEXT_EN: 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).get_combined_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_inherit_replace_node(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """<hello>
<world></world>
<world><t t-esc="hello"/></world>
<world></world>
</hello>
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """<xpath expr="/hello/world[1]" position="replace">
<world>Is a ghetto</world>
<world>Wonder when I'll find paradise</world>
</xpath>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
# First world - has been replaced by inheritance
[initial] = arch.xpath('/hello[1]/world[1]')
self.assertEqual(
'/xpath/world[1]',
initial.get('data-oe-xpath'),
'Inherited nodes have correct xpath')
# Second world added by inheritance
[initial] = arch.xpath('/hello[1]/world[2]')
self.assertEqual(
'/xpath/world[2]',
initial.get('data-oe-xpath'),
'Inherited nodes have correct xpath')
# Third world - is not editable
[initial] = arch.xpath('/hello[1]/world[3]')
self.assertFalse(
initial.get('data-oe-xpath'),
'node containing t-esc is not branded')
# The most important assert
# Fourth world - should have a correct oe-xpath, which is 3rd in main view
[initial] = arch.xpath('/hello[1]/world[4]')
self.assertEqual(
'/hello[1]/world[3]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
def test_branding_inherit_replace_node2(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """<hello>
<world></world>
<world><t t-esc="hello"/></world>
<world></world>
</hello>
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """<xpath expr="/hello/world[1]" position="replace">
<war>Is a ghetto</war>
<world>Wonder when I'll find paradise</world>
</xpath>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
[initial] = arch.xpath('/hello[1]/war[1]')
self.assertEqual(
'/xpath/war',
initial.get('data-oe-xpath'),
'Inherited nodes have correct xpath')
# First world: from inheritance
[initial] = arch.xpath('/hello[1]/world[1]')
self.assertEqual(
'/xpath/world',
initial.get('data-oe-xpath'),
'Inherited nodes have correct xpath')
# Second world - is not editable
[initial] = arch.xpath('/hello[1]/world[2]')
self.assertFalse(
initial.get('data-oe-xpath'),
'node containing t-esc is not branded')
# The most important assert
# Third world - should have a correct oe-xpath, which is 3rd in main view
[initial] = arch.xpath('/hello[1]/world[3]')
self.assertEqual(
'/hello[1]/world[3]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
def test_branding_inherit_remove_node(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
# The t-esc node is to ensure branding is distributed to both
# <world/> elements from the start
'arch': """
<hello>
<world></world>
<world></world>
<t t-esc="foo"/>
</hello>
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<data>
<xpath expr="/hello/world[1]" position="replace"/>
</data>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
# Only remaining world but still the second in original view
[initial] = arch.xpath('/hello[1]/world[1]')
self.assertEqual(
'/hello[1]/world[2]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
def test_branding_inherit_remove_node2(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """
<hello>
<world></world>
<world></world>
</hello>
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<data>
<xpath expr="/hello/world[1]" position="replace"/>
</data>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
# Note: this test is a variant of the test_branding_inherit_remove_node
# -> in this case, we expect the branding to not be distributed on the
# <hello/> element anymore but on the only remaining world.
[initial] = arch.xpath('/hello[1]')
self.assertIsNone(
initial.get('data-oe-model'),
"The inner content of the root was xpath'ed, it should not receive branding anymore")
# Only remaining world but still the second in original view
[initial] = arch.xpath('/hello[1]/world[1]')
self.assertEqual(
'/hello[1]/world[2]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
def test_branding_inherit_multi_replace_node(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """
<hello>
<world class="a"></world>
<world class="b"></world>
<world class="c"></world>
</hello>
"""
})
view2 = self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<data>
<xpath expr="//world" position="replace">
<world class="new_a"></world>
<world class="z"></world>
</xpath>
</data>
"""
})
self.View.create({ # Inherit from the child view and target the added element
'name': "Extension",
'type': 'qweb',
'inherit_id': view2.id,
'arch': """
<data>
<xpath expr="//world[hasclass('new_a')]" position="replace">
<world class="another_new_a"></world>
</xpath>
</data>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
# Check if the replacement inside the child view did not mess up the
# branding of elements in that child view
[initial] = arch.xpath('//world[hasclass("z")]')
self.assertEqual(
'/data/xpath/world[2]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
# Check if the replacement of the first worlds did not mess up the
# branding of the last world.
[initial] = arch.xpath('//world[hasclass("c")]')
self.assertEqual(
'/hello[1]/world[3]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
def test_branding_inherit_multi_replace_node2(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """
<hello>
<world class="a"></world>
<world class="b"></world>
<world class="c"></world>
</hello>
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<data>
<xpath expr="//world" position="replace">
<world class="new_a"></world>
<world class="z"></world>
</xpath>
</data>
"""
})
self.View.create({ # Inherit from the parent view but actually target
# the element added by the first child view
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<data>
<xpath expr="//world" position="replace">
<world class="another_new_a"></world>
</xpath>
</data>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
# Check if the replacement inside the child view did not mess up the
# branding of elements in that child view
[initial] = arch.xpath('//world[hasclass("z")]')
self.assertEqual(
'/data/xpath/world[2]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
# Check if the replacement of the first worlds did not mess up the
# branding of the last world.
[initial] = arch.xpath('//world[hasclass("c")]')
self.assertEqual(
'/hello[1]/world[3]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
def test_branding_inherit_remove_added_from_inheritance(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """
<hello>
<world class="a"></world>
<world class="b"></world>
</hello>
"""
})
view2 = self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
# Note: class="x" instead of t-field="x" in this arch, should lead
# to the same result that this test is ensuring but was actually
# a different case in old stable versions.
'arch': """
<data>
<xpath expr="//world[hasclass('a')]" position="after">
<world t-field="x"></world>
<world class="y"></world>
</xpath>
</data>
"""
})
self.View.create({ # Inherit from the child view and target the added element
'name': "Extension",
'type': 'qweb',
'inherit_id': view2.id,
'arch': """
<data>
<xpath expr="//world[@t-field='x']" position="replace"/>
</data>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
# Check if the replacement inside the child view did not mess up the
# branding of elements in that child view, should not be the case as
# that root level branding is not distributed.
[initial] = arch.xpath('//world[hasclass("y")]')
self.assertEqual(
'/data/xpath/world[2]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
# Check if the child view replacement of added nodes did not mess up
# the branding of last world in the parent view.
[initial] = arch.xpath('//world[hasclass("b")]')
self.assertEqual(
'/hello[1]/world[2]',
initial.get('data-oe-xpath'),
"The node's xpath position should be correct")
def test_branding_inherit_remove_node_processing_instruction(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """
<html>
<head>
<hello></hello>
</head>
<body>
<world></world>
</body>
</html>
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<data>
<xpath expr="//hello" position="replace"/>
<xpath expr="//world" position="replace"/>
</data>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
head = arch.xpath('//head')[0]
head_child = head[0]
self.assertEqual(
head_child.target,
'apply-inheritance-specs-node-removal',
"A node was removed at the start of the <head>, a processing instruction should exist as first child node")
self.assertEqual(
head_child.text,
'hello',
"The processing instruction should mention the tag of the node that was removed")
body = arch.xpath('//body')[0]
body_child = body[0]
self.assertEqual(
body_child.target,
'apply-inheritance-specs-node-removal',
"A node was removed at the start of the <body>, a processing instruction should exist as first child node")
self.assertEqual(
body_child.text,
'world',
"The processing instruction should mention the tag of the node that was removed")
self.View.distribute_branding(arch)
# Test that both head and body have their processing instruction
# 'apply-inheritance-specs-node-removal' removed after branding
# distribution. Note: test head and body separately as the code in
# charge of the removal is different in each case.
self.assertEqual(
len(head),
0,
"The processing instruction of the <head> should have been removed")
self.assertEqual(
len(body),
0,
"The processing instruction of the <body> should have been removed")
def test_branding_inherit_top_t_field(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """
<hello>
<world></world>
<world t-field="a"/>
<world></world>
<world></world>
</hello>
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<xpath expr="/hello/world[3]" position="after">
<world t-field="b"/>
</xpath>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
# First t-field should have an indication of xpath
[node] = arch.xpath('//*[@t-field="a"]')
self.assertEqual(
node.get('data-oe-xpath'),
'/hello[1]/world[2]',
'First t-field has indication of xpath')
# Second t-field, from inheritance, should also have an indication of xpath
[node] = arch.xpath('//*[@t-field="b"]')
self.assertEqual(
node.get('data-oe-xpath'),
'/xpath/world',
'Inherited t-field has indication of xpath')
# The most important assert
# The last world xpath should not have been impacted by the t-field from inheritance
[node] = arch.xpath('//world[last()]')
self.assertEqual(
node.get('data-oe-xpath'),
'/hello[1]/world[4]',
"The node's xpath position should be correct")
# Also test inherit via non-xpath t-field node, direct children of data,
# is not impacted by the feature
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<data>
<world t-field="a" position="replace">
<world t-field="z"/>
</world>
</data>
"""
})
arch_string = view1.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
node = arch.xpath('//world')[1]
self.assertEqual(
node.get('t-field'),
'z',
"The node has properly been replaced")
def test_branding_primary_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',
'mode': 'primary',
'inherit_id': view1.id,
'arch': """<xpath expr="//item" position="after">
<item order="2"/>
</xpath>
"""
})
arch_string = view2.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
[initial] = arch.xpath('//item[@order=1]')
self.assertEqual(
initial.get('data-oe-id'),
str(view1.id),
"initial should come from the root view")
self.assertEqual(
initial.get('data-oe-xpath'),
'/root[1]/item[1]',
"initial's xpath should be within the inherited view only")
[second] = arch.xpath('//item[@order=2]')
self.assertEqual(
second.get('data-oe-id'),
str(view2.id),
"second should come from the extension view")
self.assertEqual(
second.get('data-oe-xpath'),
'/xpath/item',
"second xpath should be on the inheriting view only")
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).get_combined_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_branding_attribute_groups(self):
view = self.View.create({
'name': "Base View",
'type': 'qweb',
'arch': """<root>
<item groups="base.group_no_one"/>
</root>""",
})
arch_string = view.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
self.assertEqual(arch, E.root(E.item({
'groups': 'base.group_no_one',
'data-oe-model': 'ir.ui.view',
'data-oe-id': str(view.id),
'data-oe-field': 'arch',
'data-oe-xpath': '/root[1]/item[1]',
})))
def test_call_no_branding(self):
view = self.View.create({
'name': "Base View",
'type': 'qweb',
'arch': """<root>
<item><span t-call="foo"/></item>
</root>""",
})
arch_string = view.with_context(inherit_branding=True).get_combined_arch()
arch = etree.fromstring(arch_string)
self.View.distribute_branding(arch)
self.assertEqual(arch, E.root(E.item(E.span({'t-call': "foo"}))))
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).get_combined_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).get_combined_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)
if 'arch_db' in kw:
arch_db = kw['arch_db']
if kw.get('inherit_id'):
self.cr.execute('SELECT type FROM ir_ui_view WHERE id = %s', [kw['inherit_id']])
kw['type'] = self.cr.fetchone()[0]
else:
kw['type'] = etree.fromstring(arch_db).tag
kw['arch_db'] = Json({'en_US': arch_db}) if self.env.lang == 'en_US' else Json({'en_US': arch_db, self.env.lang: arch_db})
keys = sorted(kw)
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_view_root_node_matches_view_type(self):
view = self.View.create({
'name': 'foo',
'model': 'ir.ui.view',
'arch': """
<form>
</form>
""",
})
self.assertEqual(view.type, 'form')
with self.assertRaises(ValidationError):
self.View.create({
'name': 'foo',
'model': 'ir.ui.view',
'type': 'form',
'arch': """
<data>
<div>
</div>
<form>
</form>
</data>
""",
})
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">
<separator name="separator" string="Separator" colspan="4"/>
<footer>
<button name="action_archive" type="object" string="Next button" class="btn-primary"/>
<button string="Skip" special="cancel" class="btn-secondary"/>
</footer>
</form>
"""
})
view2 = self.View.create({
'name': "edmund",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': """
<data>
<form position="attributes">
<attribute name="string">Replacement title</attribute>
</form>
<footer position="replace">
<footer>
<button name="action_archive" 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]).get_view(view2.id, '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_archive", type="object", string="New button"),
thing="bob lolo bibi and co", otherthing="lolo"
),
string="Replacement title"))
def test_view_inheritance_text_inside(self):
""" Test view inheritance when adding elements and text. """
view1 = self.View.create({
'name': "alpha",
'model': 'ir.ui.view',
'arch': '<form string="F">(<div/>)</form>',
})
view2 = self.View.create({
'name': "beta",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': '<div position="inside">a<p/>b<p/>c</div>',
})
view = self.View.with_context(check_view_ids=view2.ids).get_view(view1.id)
self.assertEqual(
view['arch'],
'<form string="F">(<div>a<p/>b<p/>c</div>)</form>',
)
def test_view_inheritance_text_after(self):
""" Test view inheritance when adding elements and text. """
view1 = self.View.create({
'name': "alpha",
'model': 'ir.ui.view',
'arch': '<form string="F">(<div/>)</form>',
})
view2 = self.View.create({
'name': "beta",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': '<div position="after">a<p/>b<p/>c</div>',
})
view = self.View.with_context(check_view_ids=view2.ids).get_view(view1.id)
self.assertEqual(
view['arch'],
'<form string="F">(<div/>a<p/>b<p/>c)</form>',
)
def test_view_inheritance_text_before(self):
""" Test view inheritance when adding elements and text. """
view1 = self.View.create({
'name': "alpha",
'model': 'ir.ui.view',
'arch': '<form string="F">(<div/>)</form>',
})
view2 = self.View.create({
'name': "beta",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': '<div position="before">a<p/>b<p/>c</div>',
})
view = self.View.with_context(check_view_ids=view2.ids).get_view(view1.id)
self.assertEqual(
view['arch'],
'<form string="F">(a<p/>b<p/>c<div/>)</form>',
)
def test_view_inheritance_divergent_models(self):
view1 = self.View.create({
'name': "bob",
'model': 'ir.ui.view.custom',
'arch': """
<form string="Base title">
<separator name="separator" string="Separator" colspan="4"/>
<footer>
<button name="action_archive" type="object" string="Next button" class="btn-primary"/>
<button string="Skip" special="cancel" class="btn-secondary"/>
</footer>
</form>
"""
})
view2 = self.View.create({
'name': "edmund",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': """
<data>
<form position="attributes">
<attribute name="string">Replacement title</attribute>
</form>
<footer position="replace">
<footer>
<button name="action_unarchive" 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]) \
.get_view(view2.id, 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_unarchive", type="object", string="New button")),
string="Replacement title"
))
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_invalid_field(self):
self.assertInvalid("""
<form string="View">
<field name="name"/>
<field name="not_a_field"/>
</form>
""", 'Field "not_a_field" does not exist in model "ir.ui.view"')
self.assertInvalid("""
<form string="View">
<field/>
</form>
""", 'Field tag must have a "name" attribute defined')
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_invalid_subfield(self):
arch = """
<form string="View">
<field name="name"/>
<field name="inherit_children_ids">
<tree name="Children">
<field name="name"/>
<field name="not_a_field"/>
</tree>
</field>
</form>
"""
self.assertInvalid(
arch,
'''Field "not_a_field" does not exist in model "ir.ui.view"''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_context_in_view(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_id" context="{'stuff': model}"/>
</form>
"""
self.assertValid(arch % '<field name="model"/>')
self.assertInvalid(
arch % '',
"""Field 'model' used in context ({'stuff': model}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_context_in_subview(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id" context="{'stuff': model}"/>
</form>
</field>
</form>
"""
self.assertValid(arch % ('', '<field name="model"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in context ({'stuff': model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="model"/>', ''),
"""Field 'model' used in context ({'stuff': model}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_context_in_subview_with_parent(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id" context="{'stuff': parent.model}"/>
</form>
</field>
</form>
"""
self.assertValid(arch % ('<field name="model"/>', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in context ({'stuff': parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>'),
"""Field 'model' used in context ({'stuff': parent.model}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_context_in_subsubview_with_parent(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id" context="{'stuff': parent.parent.model}"/>
</form>
</field>
</form>
</field>
</form>
"""
self.assertValid(arch % ('<field name="model"/>', '', ''))
self.assertInvalid(
arch % ('', '', ''),
"""Field 'model' used in context ({'stuff': parent.parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>', ''),
"""Field 'model' used in context ({'stuff': parent.parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '', '<field name="model"/>'),
"""Field 'model' used in context ({'stuff': parent.parent.model}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_id_case(self):
# id is read by default and should be usable in domains
self.assertValid("""
<form string="View">
<field name="inherit_id" domain="[('id', '=', False)]"/>
</form>
""")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_boolean_case(self):
arch = """
<form string="View">
%s
<field name="inherit_id" domain="[(%s, '=', %s)]"/>
</form>
"""
self.assertValid(arch % ('', '1', '1'))
self.assertValid(arch % ('', '0', '1'))
# self.assertInvalid(arch % ('', '1', '0'))
self.assertValid(arch % ('<field name="name"/>', '1', '0 if name else 1'))
# self.assertInvalid(arch % ('<field name="name"/><field name="type"/>', "'tata' if name else 'tutu'", 'type'), 'xxxx')
self.assertInvalid(
arch % ('', '1', '0 if name else 1'),
"""Field 'name' used in domain of <field name="inherit_id"> ([(1, '=', 0 if name else 1)]) must be present in view but is missing""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_in_view(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_id" domain="[('model', '=', model)]"/>
</form>
"""
self.assertValid(arch % '<field name="model"/>')
self.assertInvalid(
arch % '',
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', model)]) must be present in view but is missing.""",
)
def test_domain_unknown_field(self):
self.assertInvalid("""
<form string="View">
<field name="name"/>
<field name="inherit_id" domain="[('invalid_field', '=', 'res.users')]"/>
</form>
""",
'''Unknown field "ir.ui.view.invalid_field" in domain of <field name="inherit_id"> ([('invalid_field', '=', 'res.users')])''',
)
def test_domain_field_searchable(self):
arch = """
<form string="View">
<field name="name"/>
<field name="inherit_id" domain="[('%s', '=', 'test')]"/>
</form>
"""
# computed field with a search method
self.assertValid(arch % 'model_data_id')
# computed field, not stored, no search
self.assertInvalid(
arch % 'xml_id',
'''Unsearchable field 'xml_id' in path 'xml_id' in domain of <field name="inherit_id"> ([('xml_id', '=', 'test')])''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_field_no_comodel(self):
self.assertInvalid("""
<form string="View">
<field name="name" domain="[('test', '=', 'test')]"/>
</form>
""", "Domain on non-relational field \"name\" makes no sense (domain:[('test', '=', 'test')])")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_in_subview(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id" domain="[('model', '=', model)]"/>
</form>
</field>
</form>
"""
self.assertValid(arch % ('', '<field name="model"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="model"/>', ''),
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_in_subview_with_parent(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id" domain="[('model', '=', parent.model)]"/>
</form>
</field>%s
</form>
"""
self.assertValid(arch % ('<field name="model"/>', '', ''))
self.assertValid(arch % ('', '', '<field name="model"/>'))
self.assertInvalid(
arch % ('', '', ''),
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>', ''),
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_field_in_view(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_id"/>
</form>
"""
self.assertValid(arch % '<field name="model"/>')
self.assertInvalid(
arch % '',
"""Field 'model' used in domain of python field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_field_in_subview(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id"/>
</form>
</field>
</form>
"""
self.assertValid(arch % ('', '<field name="model"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in domain of python field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="model"/>', ''),
"""Field 'model' used in domain of python field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_field_in_subview_with_parent(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', parent.model)]")
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id"/>
</form>
</field>
</form>
"""
self.assertValid(arch % ('<field name="model"/>', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in domain of python field 'inherit_id' ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>'),
"""Field 'model' used in domain of python field 'inherit_id' ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_field_in_noneditable_subview(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
arch = """
<form string="View">
<field name="name"/>
<field name="inherit_children_ids">
<tree string="Children"%s>
<field name="name"/>
<field name="inherit_id"/>
</tree>
</field>
</form>
"""
self.assertValid(arch % '')
self.assertInvalid(
arch % ' editable="bottom"',
"""Field 'model' used in domain of python field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_readonly_field_in_view(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
arch = """
<form string="View">
<field name="name"/>
<field name="inherit_id" readonly="1"/>
</form>
"""
self.assertValid(arch)
self.patch(field, 'readonly', True)
arch = """
<form string="View">
<field name="name"/>
<field name="inherit_id"/>
</form>
"""
self.assertValid(arch)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_readonly_field_in_subview(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
arch = """
<form string="View">
<field name="name"/>
<field name="inherit_children_ids"%s>
<form string="Children">
<field name="name"/>
<field name="inherit_id"/>
</form>
</field>
</form>
"""
self.assertValid(arch % ' readonly="1"')
self.assertInvalid(
arch % '',
"""Field 'model' used in domain of python field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_in_filter(self):
arch = """
<search string="Search">
<field name="%s"/>
<filter string="Dummy" name="draft" domain="[('%s', '=', 'dummy')]"/>
</search>
"""
self.assertValid(arch % ('name', 'name'))
self.assertValid(arch % ('name', 'inherit_children_ids.name'))
self.assertInvalid(
arch % ('invalid_field', 'name'),
'Field "invalid_field" does not exist in model "ir.ui.view"',
)
self.assertInvalid(
arch % ('name', 'invalid_field'),
"""Unknown field "ir.ui.view.invalid_field" in domain of <filter name="draft"> ([('invalid_field', '=', 'dummy')])""",
)
self.assertInvalid(
arch % ('name', 'inherit_children_ids.invalid_field'),
"""Unknown field "ir.ui.view.invalid_field" in domain of <filter name="draft"> ([('inherit_children_ids.invalid_field', '=', 'dummy')])""",
)
# todo add check for non searchable fields and group by
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_group_by_in_filter(self):
arch = """
<search string="Search">
<filter string="Date" name="month" domain="[]" context="{'group_by':'%s'}"/>
</search>
"""
self.assertValid(arch % 'name')
self.assertInvalid(
arch % 'invalid_field',
"""Unknown field "invalid_field" in "group_by" value in context="{'group_by':'invalid_field'}""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_invalid_in_filter(self):
# invalid domain: it should be a list of tuples
self.assertInvalid(
""" <search string="Search">
<filter string="Dummy" name="draft" domain="['name', '=', 'dummy']"/>
</search>
""",
'''Invalid domain of <filter name="draft">: "['name', '=', 'dummy']"''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_searchpanel(self):
arch = """
<search>
%s
<searchpanel>
%s
<field name="groups_id" select="multi" domain="[('%s', '=', %s)]" enable_counters="1"/>
</searchpanel>
</search>
"""
self.assertValid(arch % ('', '<field name="inherit_id"/>', 'view_access', 'inherit_id'))
self.assertInvalid(
arch % ('<field name="inherit_id"/>', '', 'view_access', 'inherit_id'),
"""Field 'inherit_id' used in domain of <field name="groups_id"> ([('view_access', '=', inherit_id)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="inherit_id"/>', 'view_access', 'view_access'),
"""Field 'view_access' used in domain of <field name="groups_id"> ([('view_access', '=', view_access)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="inherit_id"/>', 'inherit_id', 'inherit_id'),
"""Unknown field "res.groups.inherit_id" in domain of <field name="groups_id"> ([('inherit_id', '=', inherit_id)])""",
)
self.assertInvalid(
arch % ('', '<field name="inherit_id" select="multi"/>', 'view_access', 'inherit_id'),
"""Field 'inherit_id' used in domain of <field name="groups_id"> ([('view_access', '=', inherit_id)]) is present in view but is in select multi.""",
)
arch = """
<search>
<searchpanel>
<field name="inherit_id" enable_counters="1"/>
</searchpanel>
<searchpanel>
<field name="inherit_id" enable_counters="1"/>
</searchpanel>
</search>
"""
self.assertInvalid(arch, "Search tag can only contain one search panel")
def test_groups_field(self):
arch = """
<form string="View">
<field name="name" groups="%s"/>
</form>
"""
self.assertValid(arch % 'base.group_no_one')
self.assertWarning(arch % 'base.dummy')
def test_attrs_groups_behavior(self):
view = self.View.create({
'name': 'foo',
'model': 'res.partner',
'arch': """
<form>
<field name="name"/>
<field name="company_id" groups="base.group_system"/>
<div id="foo"/>
<div id="bar" groups="base.group_system"/>
</form>
""",
})
user_demo = self.user_demo
# Make sure demo doesn't have the base.group_system
self.assertFalse(self.env['res.partner'].with_user(user_demo).env.user.has_group('base.group_system'))
arch = self.env['res.partner'].with_user(user_demo).get_view(view_id=view.id)['arch']
tree = etree.fromstring(arch)
self.assertTrue(tree.xpath('//field[@name="name"]'))
self.assertFalse(tree.xpath('//field[@name="company_id"]'))
self.assertTrue(tree.xpath('//div[@id="foo"]'))
self.assertFalse(tree.xpath('//div[@id="bar"]'))
user_admin = self.env.ref('base.user_admin')
# Make sure admin has the base.group_system
self.assertTrue(self.env['res.partner'].with_user(user_admin).env.user.has_group('base.group_system'))
arch = self.env['res.partner'].with_user(user_admin).get_view(view_id=view.id)['arch']
tree = etree.fromstring(arch)
self.assertTrue(tree.xpath('//field[@name="name"]'))
self.assertTrue(tree.xpath('//field[@name="company_id"]'))
self.assertTrue(tree.xpath('//div[@id="foo"]'))
self.assertTrue(tree.xpath('//div[@id="bar"]'))
def test_attrs_groups_validation(self):
def validate(arch, valid=False, parent=False):
parent = 'parent.' if parent else ''
if valid:
self.assertValid(arch % {'attrs': f"""invisible="{parent}name == 'foo'" """})
self.assertValid(arch % {'attrs': f"""domain="[('name', '!=', {parent}name)]" """})
self.assertValid(arch % {'attrs': f"""context="{{'default_name': {parent}name}}" """})
self.assertValid(arch % {'attrs': f"""decoration-info="{parent}name == 'foo'" """})
else:
self.assertInvalid(
arch % {'attrs': f"""invisible="{parent}name == 'foo'" """},
f"""Field 'name' used in modifier 'invisible' ({parent}name == 'foo') is restricted to the group(s)""",
)
self.assertInvalid(
arch % {'attrs': f"""domain="[('name', '!=', {parent}name)]" """},
f"""Field 'name' used in domain of <field name="inherit_id"> ([('name', '!=', {parent}name)]) is restricted to the group(s)""",
)
self.assertInvalid(
arch % {'attrs': f"""context="{{'default_name': {parent}name}}" """},
f"""Field 'name' used in context ({{'default_name': {parent}name}}) is restricted to the group(s)""",
)
self.assertInvalid(
arch % {'attrs': f"""decoration-info="{parent}name == 'foo'" """},
f"""Field 'name' used in decoration-info="{parent}name == 'foo'" is restricted to the group(s)""",
)
# Assert using a field restricted to a group
# in another field without the same group is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" %(attrs)s/>
</form>
""", valid=False)
# Assert using a parent field restricted to a group
# in a child field without the same group is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" %(attrs)s/>
</tree>
</field>
</form>
""", valid=False, parent=True)
# Assert using a parent field restricted to a group
# in a child field with the same group is valid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a parent field available for everyone
# in a child field restricted to a group is valid
validate("""
<form string="View">
<field name="name"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a field available for everyone
# in another field restricted to a group is valid
validate("""
<form string="View">
<field name="name"/>
<field name="inherit_id" %(attrs)s groups="base.group_system"/>
</form>
""", valid=True)
# Assert using a field restricted to a group
# in another field with the same group is valid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field available twice for 2 diffent groups
# in another field restricted to one of the 2 groups is valid
validate("""
<form string="View">
<field name="name" groups="base.group_portal"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to a group only
# in other fields restricted to at least one different group is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
<field name="inherit_id" groups="base.group_portal" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field available twice for 2 different groups
# in other fields restricted to the same 2 group is valid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="name" groups="base.group_portal"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
<field name="inherit_id" groups="base.group_portal" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field available for 2 diffent groups,
# in another field restricted to one of the 2 groups is valid
validate("""
<form string="View">
<field name="name" groups="base.group_portal,base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field available for 1 group only
# in another field restricted 2 groups is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_portal,base.group_system" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field restricted to a group
# in another field restricted to a group including the group for which the field is available is valid
validate("""
<form string="View">
<field name="name" groups="base.group_erp_manager"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a parent field restricted to a group
# in a child field restricted to a group including the group for which the field is available is valid
validate("""
<form string="View">
<field name="name" groups="base.group_erp_manager"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a field restricted to a group
# in another field restricted to a group not including the group for which the field is available is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_erp_manager" %(attrs)s/>
</form>
""", valid=False)
# Assert using a parent field restricted to a group
# in a child field restricted to a group not including the group for which the field is available is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" groups="base.group_erp_manager" %(attrs)s/>
</tree>
</field>
</form>
""", valid=False, parent=True)
# Assert using a field within a block restricted to a group
# in another field not restricted to the same group is invalid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
</group>
<field name="inherit_id" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field within a block restricted to a group
# in another field within the same block restricted to a group is valid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=True)
# Assert using a field within a block restricted to a group
# in another field within the same block restricted to a group and additional groups on the field node is valid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
<field name="inherit_id" %(attrs)s groups="base.group_multi_currency,base.group_multi_company"/>
</group>
</form>
""", valid=True)
# Assert using a field within a block restricted to a group
# in another field within a block restricted to the same group is valid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
</group>
<group groups="base.group_system">
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=True)
# Assert using a field within a block restricted to a group
# in another field within a block restricted to a group including the group for which the field is available
# is valid
validate("""
<form string="View">
<group groups="base.group_erp_manager">
<field name="name"/>
</group>
<group groups="base.group_system">
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=True)
# Assert using a field within a block restricted to a group
# in another field within a block restricted to a group not including the group for which the field is available
# is invalid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
</group>
<group groups="base.group_erp_manager">
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=False)
# Assert using a parent field restricted to a group
# in a child field under a relational field restricted to the same group is valid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids" groups="base.group_system">
<tree editable="bottom">
<field name="inherit_id" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a parent field restricted to a group
# in a child field under a relational field restricted
# to a group including the group for which the field is available is valid
validate("""
<form string="View">
<field name="name" groups="base.group_erp_manager"/>
<field name="inherit_children_ids" groups="base.group_system">
<tree editable="bottom">
<field name="inherit_id" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a parent field restricted to a group
# in a child field under a relational field restricted
# to a group not including the group for which the field is available is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids" groups="base.group_erp_manager">
<tree editable="bottom">
<field name="inherit_id" %(attrs)s/>
</tree>
</field>
</form>
""", valid=False, parent=True)
# Assert using a field restricted to users not having a group
# in another field not restricted to any group is invalid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="inherit_id" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field not restricted to any group
# in another field restricted to users not having a group is valid
validate("""
<form string="View">
<field name="name"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to users not having multiple groups
# in another field restricted to users not having one of the group only is invalid
# e.g.
# if the user is portal, the field "name" will not be in the view
# but the field "inherit_id" where "name" is used will be in the view
# making it invalid.
validate("""
<form string="View">
<field name="name" groups="!base.group_system,!base.group_portal"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field restricted to users not having a group
# in another field restricted to users not having multiple group including the one above is valid
# e.g.
# if the user is portal, the field "name" will be in the view
# but the field "inherit_id" where "name" is used will not be in the view
# making it valid.
validate("""
<form string="View">
<field name="name" groups="!base.group_user"/>
<field name="inherit_id" groups="!base.group_user,!base.group_portal" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to a non group
# in another field for which the non group is not implied is invalid
# e.g.
# if the user is employee, the field "name" will not be in the view
# but the field "inherit_id" where "name" is used will be in the view,
# making it invalid.
validate("""
<form string="View">
<field name="name" groups="!base.group_user"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field restricted to a non group
# in another field restricted to a non group implied in the non group of the available field is valid
# e.g.
# if the user is employee, the field "name" will be in the view
# but the field "inherit_id", where "name" is used, will not be in the view,
# therefore making it valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="inherit_id" groups="!base.group_user" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to non-admins, itself in a block restricted to employees,
# in another field restricted to a block restricted to employees
# is invalid
# e.g.
# if the user is admin, the field "name" will not be in the view
# but the field "inherit_id", where "name" is used, will be in the view,
# threfore making it invalid
validate("""
<form string="View">
<group groups="base.group_user">
<field name="name" groups="!base.group_system"/>
</group>
<group groups="base.group_user">
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=False)
# Assert using a field restricted to a group
# in another field restricted the opposite group is invalid
# e.g.
# if the user is admin, the field "name" will be in the view
# but the field "inherit_id", where "name" is used, will not be in the view,
# therefore making it invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=False)
# Assert having two times the same field with a mutually exclusive group
# and using that field in another field without any group is valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" %(attrs)s/>
</form>
""", valid=True)
# Assert having two times the same field with a mutually exclusive group
# and using that field in another field using the group is valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert having two times the same field with a mutually exclusive group
# and using that field in another field using the !group is valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert having two times the same field with a mutually exclusive group
# and using that field in another field restricted to any other group is valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_portal" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to a 'base.group_no_one' in another
# field with a group implied 'base.group_no_one' is invalid. The group
# 'base.group_no_one' must be in the view because it's depending of the
# session.
validate("""
<form string="View">
<field name="name" groups="base.group_no_one"/>
<field name="inherit_id" %(attrs)s groups="base.group_user"/>
</form>
""", valid=False)
validate("""
<form string="View">
<field name="name" groups="base.group_no_one"/>
<group groups="base.group_no_one">
<field name="inherit_id" %(attrs)s groups="base.group_user"/>
</group>
</form>
""", valid=True)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_empty_groups_attrib(self):
"""Ensure we allow empty groups attribute"""
view = self.View.create({
'name': 'foo',
'model': 'res.partner',
'arch': """
<form>
<field name="name" groups="" />
</form>
""",
})
arch = self.env['res.partner'].get_view(view_id=view.id)['arch']
tree = etree.fromstring(arch)
nodes = tree.xpath("//field[@name='name' and not (@groups)]")
self.assertEqual(1, len(nodes))
def test_invisible_groups_with_groups_in_model(self):
"""Tests the attrs is well processed to modifiers for a field node combining:
- a `groups` attribute on the field node in the view architecture
- a `groups` attribute on the field in the Python model
This is an edge case and it worths a unit test."""
self.patch(type(self.env['res.partner']).name, 'groups', 'base.group_system')
self.env.user.groups_id += self.env.ref('base.group_multi_company')
view = self.View.create({
'name': 'foo',
'model': 'res.partner',
'arch': """
<form>
<field name="active"/>
<field name="name" groups="base.group_multi_company" invisible="active"/>
</form>
""",
})
arch = self.env['res.partner'].get_view(view_id=view.id)['arch']
tree = etree.fromstring(arch)
node_field_name = tree.xpath('//field[@name="name"]')[0]
self.assertEqual(node_field_name.get('invisible'), "active")
def test_button(self):
arch = """
<form>
<button type="object" name="%s"/>
</form>
"""
self.assertValid(arch % 'action_archive', name='valid button name')
self.assertInvalid(
arch % 'wtfzzz', 'wtfzzz is not a valid action on ir.ui.view',
name='button name is not even a method',
)
self.assertInvalid(
arch % '_check_xml',
'_check_xml on ir.ui.view is private and cannot be called from a button',
name='button name is a private method',
)
self.assertWarning(arch % 'postprocess_and_fields', name='button name is a method that requires extra arguments')
arch = """
<form>
<button type="action" name="%s"/>
</form>
"""
self.assertInvalid(arch % 0, 'Action 0 (id: 0) does not exist for button of type action.')
self.assertInvalid(arch % 'base.random_xmlid', 'Invalid xmlid base.random_xmlid for button of type action')
self.assertInvalid('<form><button type="action"/></form>', 'Button must have a name')
self.assertInvalid('<form><button special="dummy"/></form>', "Invalid special 'dummy' in button")
self.assertInvalid(arch % 'base.partner_root', "base.partner_root is of type res.partner, expected a subclass of ir.actions.actions")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_tree(self):
arch = """
<tree>
<field name="name"/>
<button type='object' name="action_archive"/>
%s
</tree>
"""
self.assertValid(arch % '')
self.assertInvalid(arch % '<group/>', "Tree child can only have one of field, button, control, groupby, widget, header tag (not group)")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_tree_groupby(self):
arch = """
<tree>
<field name="name"/>
<groupby name="%s">
<button type="object" name="action_archive"/>
</groupby>
</tree>
"""
self.assertValid(arch % ('model_data_id'))
self.assertInvalid(arch % ('type'), "Field 'type' found in 'groupby' node can only be of type many2one, found selection")
self.assertInvalid(arch % ('dummy'), "Field 'dummy' found in 'groupby' node does not exist in model ir.ui.view")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_tree_groupby_many2one(self):
arch = """
<tree>
<field name="name"/>
%s
<groupby name="model_data_id">
%s
<button type="object" name="action_archive" invisible="noupdate" string="Button1"/>
</groupby>
</tree>
"""
self.assertValid(arch % ('', '<field name="noupdate"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'noupdate' used in modifier 'invisible' (noupdate) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="noupdate"/>', ''),
'''Field "noupdate" does not exist in model "ir.ui.view"''',
)
self.assertInvalid(
arch % ('', '<field name="noupdate"/><field name="fake_field"/>'),
'''Field "fake_field" does not exist in model "ir.model.data"''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_check_xml_on_reenable(self):
view1 = self.View.create({
'name': 'valid _check_xml',
'model': 'ir.ui.view',
'arch': """
<form string="View">
<field name="name"/>
</form>
""",
})
view2 = self.View.create({
'name': 'valid _check_xml',
'model': 'ir.ui.view',
'inherit_id': view1.id,
'active': False,
'arch': """
<field name="foo" position="after">
<field name="bar"/>
</field>
"""
})
with self.assertRaises(ValidationError):
view2.active = True
# Re-enabling the view and correcting it at the same time should not raise the `_check_xml` constraint.
view2.write({
'active': True,
'arch': """
<field name="name" position="after">
<span>bar</span>
</field>
""",
})
def test_for_in_label(self):
self.assertValid('<form><field name="model"/><label for="model"/></form>')
self.assertInvalid(
'<form><field name="model"/><label/></form>',
"""Label tag must contain a "for". To match label style without corresponding field or button, use 'class="o_form_label"'""",
)
self.assertInvalid(
'<form><label for="model"/></form>',
"""Name or id 'model' in <label for="..."> must be present in view but is missing.""",
)
def test_col_colspan_numerical(self):
self.assertValid('<form><group col="5"></group></form>')
self.assertInvalid(
'<form><group col="alpha"></group></form>',
"'col' value must be an integer (alpha)",
)
self.assertValid('<form><div colspan="5"></div></form>')
self.assertInvalid(
'<form><div colspan="alpha"></div></form>',
"'colspan' value must be an integer (alpha)",
)
def test_valid_alerts(self):
self.assertValid('<form><div class="alert alert-success" role="alert"/></form>')
self.assertValid('<form><div class="alert alert-success" role="alertdialog"/></form>')
self.assertValid('<form><div class="alert alert-success" role="status"/></form>')
self.assertWarning('<form><div class="alert alert-success"/></form>')
def test_valid_prohibited_none_role(self):
self.assertWarning('<form><div role="none"/></form>')
self.assertWarning('<form><div role="presentation"/></form>')
def test_valid_alternative_image_text(self):
self.assertValid('<form><img src="a" alt="a image"></img></form>')
self.assertWarning('<form><img src="a"></img></form>')
def test_valid_accessibility_icon_text(self):
self.assertWarning(
'<form><span class="fa fa-warning"/></form>',
'A <span> with fa class (fa fa-warning) must have title in its tag, parents, descendants or have text'
)
self.assertWarning(
'<form><button icon="fa-warning"/></form>',
'A button with icon attribute (fa-warning) must have title in its tag, parents, descendants or have text'
)
self.assertValid('<form><button icon="fa-warning"/>text</form>')
self.assertValid('<form><span class="fa fa-warning"/>text</form>')
self.assertValid('<form>text<span class="fa fa-warning"/></form>')
self.assertValid('<form><span class="fa fa-warning">text</span></form>')
self.assertValid('<form><span title="text" class="fa fa-warning"/></form>')
self.assertValid('<form><span aria-label="text" class="fa fa-warning"/></form>')
def test_valid_simili_button(self):
self.assertWarning('<form><a class="btn"/></form>')
self.assertValid('<form><a class="btn" role="button"/></form>')
def test_valid_dialog(self):
self.assertWarning('<form><div class="modal"/></form>')
self.assertValid('<form><div role="dialog" class="modal"></div></form>')
self.assertWarning('<form><div class="modal-header"/></form>')
self.assertValid('<form><header class="modal-header"/></form>')
self.assertWarning('<form><div class="modal-footer"/></form>')
self.assertValid('<form><footer class="modal-footer"/></form>')
self.assertWarning('<form><div class="modal-body"/></form>')
self.assertValid('<form><main class="modal-body"/></form>')
def test_valid_simili_dropdown(self):
self.assertValid('<form><ul class="dropdown-menu" role="menu"></ul></form>')
self.assertWarning('<form><ul class="dropdown-menu"></ul></form>')
def test_valid_simili_progressbar(self):
self.assertValid('<form><div class="o_progressbar" role="progressbar" aria-valuenow="14" aria-valuemin="0" aria-valuemax="100">14%</div></form>')
self.assertWarning('<form><div class="o_progressbar" aria-valuenow="14" aria-valuemin="0" aria-valuemax="100">14%</div></form>')
self.assertWarning('<form><div class="o_progressbar" role="progressbar" aria-valuemin="0" aria-valuemax="100">14%</div></form>')
self.assertWarning('<form><div class="o_progressbar" role="progressbar" aria-valuenow="14" aria-valuemax="100">14%</div></form>')
self.assertWarning('<form><div class="o_progressbar" role="progressbar" aria-valuenow="14" aria-valuemin="0" >14%</div></form>')
def test_valid_simili_tabpanel(self):
self.assertValid('<form><div class="tab-pane" role="tabpanel"/></form>')
self.assertWarning('<form><div class="tab-pane"/></form>')
def test_valid_simili_tablist(self):
self.assertValid('<form><div class="nav-tabs" role="tablist"/></form>')
self.assertWarning('<form><div class="nav-tabs"/></form>')
def test_valid_simili_tab(self):
self.assertValid('<form><a data-bs-toggle="tab" role="tab" aria-controls="test"/></form>')
self.assertWarning('<form><a data-bs-toggle="tab" aria-controls="test"/></form>')
self.assertWarning('<form><a data-bs-toggle="tab" role="tab"/></form>')
self.assertWarning('<form><a data-bs-toggle="tab" role="tab" aria-controls="#test"/></form>')
def test_valid_focusable_button(self):
self.assertValid('<form><a class="btn" role="button"/></form>')
self.assertValid('<form><button class="btn" role="button"/></form>')
self.assertValid('<form><select class="btn" role="button"/></form>')
self.assertValid('<form><input type="button" class="btn" role="button"/></form>')
self.assertValid('<form><input type="submit" class="btn" role="button"/></form>')
self.assertValid('<form><input type="reset" class="btn" role="button"/></form>')
self.assertValid('<form><div type="reset" class="btn btn-group" role="button"/></form>')
self.assertValid('<form><div type="reset" class="btn btn-toolbar" role="button"/></form>')
self.assertValid('<form><div type="reset" class="btn btn-addr" role="button"/></form>')
self.assertWarning('<form><div class="btn" role="button"/></form>')
self.assertWarning('<form><input type="email" class="btn" role="button"/></form>')
def test_partial_validation(self):
self.View = self.View.with_context(load_all_views=True)
# base view
view0 = self.assertValid("""
<form string="View">
<field name="model"/>
<field name="inherit_id" domain="[('model', '=', model)]"/>
</form>
""")
# added elements should be validated
self.assertInvalid(
"""<form position="inside">
<field name="groups_id" domain="[('invalid_field', '=', 'dummy')]"/>
</form>""",
"""Unknown field "res.groups.invalid_field" in domain of <field name="groups_id"> ([('invalid_field', '=', 'dummy')]))""",
inherit_id=view0.id,
)
view1 = self.assertValid(
"""<form position="inside">
<field name="name"/>
</form>""",
inherit_id=view0.id,
)
view2 = self.assertValid(
"""<form position="inside">
<field name="groups_id" domain="[('name', '=', name)]"/>
<label for="groups_id"/>
</form>""",
inherit_id=view1.id,
)
# modifying attributes should validate the target element
self.assertInvalid(
"""<field name="inherit_id" position="attributes">
<attribute name="domain">[('invalid_field', '=', 'dummy')]</attribute>
</field>""",
"""Unknown field "ir.ui.view.invalid_field" in domain of <field name="inherit_id"> ([('invalid_field', '=', 'dummy')]))""",
inherit_id=view0.id,
)
# replacing an element should validate the whole view
self.assertInvalid(
"""<field name="model" position="replace"/>""",
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', model)]) must be present in view but is missing.""",
inherit_id=view0.id,
)
# moving an element should have no impact; this test checks that the
# implementation does not flag the inner element to be validated, which
# prevents to locate the corresponding element inside the arch
self.assertValid(
"""<field name="groups_id" position="before">
<label for="groups_id" position="move"/>
</field>""",
inherit_id=view2.id,
)
# modifying a view extension should validate the other views
with mute_logger('odoo.addons.base.models.ir_ui_view'):
with self.assertRaises(ValidationError):
with self.cr.savepoint():
view1.arch = """<form position="inside">
<field name="type"/>
</form>"""
def test_graph_fields(self):
self.assertValid('<graph string="Graph"><field name="model" type="row"/><field name="inherit_id" type="measure"/></graph>')
self.assertInvalid(
'<graph string="Graph"><label for="model"/><field name="model" type="row"/><field name="inherit_id" type="measure"/></graph>',
'A <graph> can only contains <field> nodes, found a <label>'
)
def test_graph_attributes(self):
self.assertValid('<graph string="Graph" cumulated="1" ><field name="model" type="row"/><field name="inherit_id" type="measure"/></graph>')
def test_view_ref(self):
view = self.assertValid(
"""
<form>
<field name="groups_id" class="canary"/>
</form>
"""
)
self.env["ir.model.data"].create({
'module': 'base',
'name': 'test_views_test_view_ref',
'model': 'ir.ui.view',
'res_id': view.id,
})
view_data = self.env['ir.ui.view'].with_context(form_view_ref='base.test_views_test_view_ref').get_view()
self.assertEqual(view.id, view_data['id'], "The view returned should be test_views_test_view_ref")
view_data = self.env['ir.ui.view'].with_context(form_view_ref='base.test_views_test_view_ref').get_view(view.id)
tree = etree.fromstring(view_data['arch'])
field_groups_id = tree.xpath('//field[@name="groups_id"]')[0]
self.assertEqual(
len(field_groups_id.xpath(".//*[@class='canary']")),
0,
"The view test_views_test_view_ref should not be in the views of the many2many field groups_id"
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_forbidden_owl_directives_in_form(self):
arch = "<form>%s</form>"
self.assertInvalid(
arch % ('<span t-esc="x"/>'),
"""Error while validating view near:
<form __validate__="1"><span t-esc="x"/></form>
Forbidden owl directive used in arch (t-esc).""",
)
self.assertInvalid(
arch % ('<span t-on-click="x.doIt()"/>'),
"""Error while validating view near:
<form __validate__="1"><span t-on-click="x.doIt()"/></form>
Forbidden owl directive used in arch (t-on-click).""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_forbidden_owl_directives_in_kanban(self):
arch = "<kanban><templates><t t-name='kanban-box'>%s</t></templates></kanban>"
self.assertValid(arch % ('<span t-esc="record.resId"/>'))
self.assertValid(arch % ('<t t-debug=""/>'))
self.assertInvalid(
arch % ('<span t-on-click="x.doIt()"/>'),
"""Error while validating view near:
<kanban __validate__="1"><templates><t t-name="kanban-box"><span t-on-click="x.doIt()"/></t></templates></kanban>
Forbidden owl directive used in arch (t-on-click).""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_forbidden_data_tooltip_attributes_in_form(self):
arch = "<form>%s</form>"
self.assertInvalid(
arch % ('<span data-tooltip="Test"/>'),
"""Error while validating view near:
<form __validate__="1"><span data-tooltip="Test"/></form>
Forbidden attribute used in arch (data-tooltip)."""
)
self.assertInvalid(
arch % ('<span data-tooltip-template="test"/>'),
"""Error while validating view near:
<form __validate__="1"><span data-tooltip-template="test"/></form>
Forbidden attribute used in arch (data-tooltip-template)."""
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_forbidden_data_tooltip_attributes_in_kanban(self):
arch = "<kanban><templates><t t-name='kanban-box'>%s</t></templates></kanban>"
self.assertInvalid(
arch % ('<span data-tooltip="Test"/>'),
"""Error while validating view near:
<kanban __validate__="1"><templates><t t-name="kanban-box"><span data-tooltip="Test"/></t></templates></kanban>
Forbidden attribute used in arch (data-tooltip)."""
)
self.assertInvalid(
arch % ('<span data-tooltip-template="test"/>'),
"""Error while validating view near:
<kanban __validate__="1"><templates><t t-name="kanban-box"><span data-tooltip-template="test"/></t></templates></kanban>
Forbidden attribute used in arch (data-tooltip-template)."""
)
self.assertInvalid(
arch % ('<span t-att-data-tooltip="test"/>'),
"""Error while validating view near:
<kanban __validate__="1"><templates><t t-name="kanban-box"><span t-att-data-tooltip="test"/></t></templates></kanban>
Forbidden attribute used in arch (t-att-data-tooltip)."""
)
self.assertInvalid(
arch % ('<span t-attf-data-tooltip-template="{{ test }}"/>'),
"""Error while validating view near:
<kanban __validate__="1"><templates><t t-name="kanban-box"><span t-attf-data-tooltip-template="{{ test }}"/></t></templates></kanban>
Forbidden attribute used in arch (t-attf-data-tooltip-template)."""
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_forbidden_use_of___comp___in_kanban(self):
arch = "<kanban><templates><t t-name='kanban-box'>%s</t></templates></kanban>"
self.assertInvalid(
arch % '<t t-esc="__comp__.props.resId"/>',
"""Error while validating view near:
<kanban __validate__="1"><templates><t t-name="kanban-box"><t t-esc="__comp__.props.resId"/></t></templates></kanban>
Forbidden use of `__comp__` in arch."""
)
@tagged('post_install', '-at_install')
class TestDebugger(common.TransactionCase):
def test_t_debug_in_qweb_based_views(self):
View = self.env['ir.ui.view']
views_with_t_debug = View.search([["arch_db", "like", "t-debug="]])
self.assertEqual([v.xml_id for v in views_with_t_debug], [])
class TestViewTranslations(common.TransactionCase):
# these tests are essentially the same as in test_translate.py, but they use
# the computed field 'arch' instead of the translated field 'arch_db'
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env['res.lang']._activate_lang('fr_FR')
cls.env['res.lang']._activate_lang('nl_NL')
cls.env['ir.module.module']._load_module_terms(['base'], ['fr_FR', 'nl_NL'])
def create_view(self, archf, terms, **kwargs):
view = self.env['ir.ui.view'].create({
'name': 'test',
'model': 'res.partner',
'arch': archf % terms,
})
# DLE P70: `_sync_terms_translations`, which delete translations for which there is no value, is called sooner than before
# because it's called in `_write`, which is called by `flush`, which is called by the `search`.
# `arch_db` is in `_write` instead of `create` because `arch_db` is the inverse of `arch`.
# We need to flush `arch_db` before creating the translations otherwise the translation for which there is no value will be deleted,
# while the `test_sync_update` specifically needs empty translations
self.env.flush_all()
val = {'en_US': archf % terms}
for lang, trans_terms in kwargs.items():
val[lang] = archf % trans_terms
query = "UPDATE ir_ui_view SET arch_db = %s WHERE id = %s"
self.env.cr.execute(query, [Json(val), view.id])
self.env.invalidate_all()
return view
def test_sync(self):
""" Check translations of 'arch' after minor change in source terms. """
archf = '<form string="X">%s</form>'
terms_en = ('Bread and cheeze',)
terms_fr = ('Pain et fromage',)
terms_nl = ('Brood and kaas',)
view = self.create_view(archf, terms_en, en_US=terms_en, fr_FR=terms_fr, nl_NL=terms_nl)
env_nolang = self.env(context={})
env_en = self.env(context={'lang': 'en_US'})
env_fr = self.env(context={'lang': 'fr_FR'})
env_nl = self.env(context={'lang': 'nl_NL'})
self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en)
self.assertEqual(view.with_env(env_en).arch, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch, archf % terms_fr)
self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl)
# modify source term in view (fixed type in 'cheeze')
terms_en = ('Bread and cheese',)
view.with_env(env_en).write({'arch': archf % terms_en})
# check whether translations have been synchronized
self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en)
self.assertEqual(view.with_env(env_en).arch, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch, archf % terms_fr)
self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl)
view = self.create_view(archf, terms_fr, en_US=terms_en, fr_FR=terms_fr, nl_NL=terms_nl)
# modify source term in view in another language with close term
new_terms_fr = ('Pains et fromage',)
view.with_env(env_fr).write({'arch': archf % new_terms_fr})
# check whether translations have been synchronized
self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en)
self.assertEqual(view.with_env(env_en).arch, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch, archf % new_terms_fr)
self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl)
def test_sync_xml(self):
""" Check translations of 'arch' after xml tags changes in source terms. """
archf = '<form string="X">%s</form>'
terms_en = ('Bread and cheese',)
terms_fr = ('Pain et fromage',)
terms_nl = ('Brood and kaas',)
view = self.create_view(archf, terms_en, en_US=terms_en, fr_FR=terms_fr, nl_NL=terms_nl)
env_nolang = self.env(context={})
env_en = self.env(context={'lang': 'en_US'})
env_fr = self.env(context={'lang': 'fr_FR'})
env_nl = self.env(context={'lang': 'nl_NL'})
self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en)
self.assertEqual(view.with_env(env_en).arch, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch, archf % terms_fr)
self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl)
# modify source term in view (add css style)
terms_en = ('Bread <span style="font-weight:bold">and</span> cheese',)
view.with_env(env_en).write({'arch': archf % terms_en})
# check whether translations have been kept
self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en)
self.assertEqual(view.with_env(env_en).arch, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch, archf % terms_fr)
self.assertEqual(view.with_env(env_nl).arch, archf % terms_nl)
# modify source term in view (actual text change)
terms_en = ('Bread <span style="font-weight:bold">and</span> butter',)
view.with_env(env_en).write({'arch': archf % terms_en})
# check whether translations have been reset
self.assertEqual(view.with_env(env_nolang).arch, archf % terms_en)
self.assertEqual(view.with_env(env_en).arch, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch, archf % terms_en)
self.assertEqual(view.with_env(env_nl).arch, archf % terms_en)
def test_sync_update(self):
""" Check translations after major changes in source terms. """
archf = '<form string="X"><div>%s</div><div>%s</div></form>'
terms_src = ('Subtotal', 'Subtotal:')
terms_en = ('', 'Sub total:')
view = self.create_view(archf, terms_src, en_US=terms_en)
# modifying the arch should sync existing translations without errors
new_arch = archf % ('Subtotal', 'Subtotal : <br/>')
view.write({"arch": new_arch})
self.assertEqual(view.arch, new_arch)
def test_cache_consistency(self):
view = self.env["ir.ui.view"].create({
"name": "test_translate_xml_cache_invalidation",
"model": "res.partner",
"arch": "<form><b>content</b></form>",
})
view_fr = view.with_context({"lang": "fr_FR"})
self.assertIn("<b>", view.arch_db)
self.assertIn("<b>", view.arch)
self.assertIn("<b>", view_fr.arch_db)
self.assertIn("<b>", view_fr.arch)
# write with no lang, and check consistency in other languages
view.write({"arch": "<form><i>content</i></form>"})
self.assertIn("<i>", view.arch_db)
self.assertIn("<i>", view.arch)
self.assertIn("<i>", view_fr.arch_db)
self.assertIn("<i>", view_fr.arch)
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')
view2.write({'inherit_id': None})
self.assertEqual(view2.mode, 'primary')
view2.write({'inherit_id': view.id})
self.assertEqual(view2.mode, 'extension')
@mute_logger('odoo.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')
self.assertEqual(view2.mode, 'primary')
with self.assertRaises(IntegrityError):
self.View.create({
'inherit_id': None,
'mode': 'extension',
'arch': '<qweb/>'
})
@mute_logger('odoo.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'})
view_pure_primary.env.flush_all()
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'})
def testChangeInheritOfPrimary(self):
"""
A primary view with an inherit_id must remain primary when changing the inherit_id
"""
base1 = self.View.create({
'inherit_id': None,
'arch': '<qweb/>',
})
base2 = self.View.create({
'inherit_id': None,
'arch': '<qweb/>',
})
view = self.View.create({
'mode': 'primary',
'inherit_id': base1.id,
'arch': '<qweb/>',
})
self.assertEqual(view.mode, 'primary')
view.write({'inherit_id': base2.id})
self.assertEqual(view.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).get_combined_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).get_combined_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).get_combined_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).get_combined_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).get_combined_arch()
self.assertEqual(
etree.fromstring(arch),
E.qweb(
E.a1(),
E.d1(),
E.b2(),
E.b1(),
E.a3(),
E.a2(),
), arch)
def test_primary_after_extensions(self):
# Here is a tricky use-case: a*
# - views a and d are primary / \
# - views b and c are extensions b c
# - depth-first order is: a, b, d, c |
# - combination order is: a, b, c, d d*
#
# The arch of d has been chosen to fail if d is applied before c.
# Because this child of 'b' is primary, it must be applied *after* the
# other extensions of a!
a = self.View.create({
'model': 'a',
'arch': '<qweb><a/></qweb>',
})
b = self.View.create({
'model': 'a',
'inherit_id': a.id,
'arch': '<a position="after"><b/></a>'
})
c = self.View.create({ # pylint: disable=unused-variable
'model': 'a',
'inherit_id': a.id,
'arch': '<a position="after"><c/></a>'
})
d = self.View.create({ # pylint: disable=unused-variable
'model': 'a',
'inherit_id': b.id,
'mode': 'primary',
'arch': '<a position="replace"/>',
})
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).get_combined_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_active()
context = {'check_view_ids': self.View.search([]).ids}
arch = self.v0.with_context(context).get_combined_arch()
self.assertEqual(
etree.fromstring(arch),
E.qweb(
E.base(),
E.v1(),
)
)
self.v3.toggle_active()
context = {'check_view_ids': self.View.search([]).ids}
arch = self.v0.with_context(context).get_combined_arch()
self.assertEqual(
etree.fromstring(arch),
E.qweb(
E.base(),
E.v1(),
E.v3(),
)
)
self.v2.toggle_active()
context = {'check_view_ids': self.View.search([]).ids}
arch = self.v0.with_context(context).get_combined_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)
class TestQWebRender(ViewCase):
def test_render(self):
view1 = self.View.create({
'name': "dummy",
'type': 'qweb',
'arch': """
<t t-name="base.dummy">
<div><span>something</span></div>
</t>
"""
})
view2 = self.View.create({
'name': "dummy_ext",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
<xpath expr="//div" position="inside">
<span>another thing</span>
</xpath>
"""
})
view3 = self.View.create({
'name': "dummy_primary_ext",
'type': 'qweb',
'inherit_id': view1.id,
'mode': 'primary',
'arch': """
<xpath expr="//div" position="inside">
<span>another primary thing</span>
</xpath>
"""
})
# render view and child view with an id
content1 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id])._render(view1.id)
content2 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id])._render(view2.id)
self.assertEqual(content1, content2)
# render view and child view with an xmlid
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
"VALUES ('dummy', 'ir.ui.view', %s, 'base')" % view1.id)
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
"VALUES ('dummy_ext', 'ir.ui.view', %s, 'base')" % view2.id)
content1 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id])._render('base.dummy')
content2 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id])._render('base.dummy_ext')
self.assertEqual(content1, content2)
# render view and primary extension with an id
content1 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id, view3.id])._render(view1.id)
content3 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id, view3.id])._render(view3.id)
self.assertNotEqual(content1, content3)
# render view and primary extension with an xmlid
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
"VALUES ('dummy_primary_ext', 'ir.ui.view', %s, 'base')" % view3.id)
content1 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id, view3.id])._render('base.dummy')
content3 = self.env['ir.qweb'].with_context(check_view_ids=[view1.id, view2.id, view3.id])._render('base.dummy_primary_ext')
self.assertNotEqual(content1, content3)
class TestValidationTools(common.BaseCase):
def test_get_expression_identities(self):
self.assertEqual(
view_validation.get_expression_field_names("context_today().strftime('%Y-%m-%d')"),
set(),
)
self.assertEqual(
view_validation.get_expression_field_names("field and field[0] or not field2"),
{'field', 'field2'},
)
self.assertEqual(
view_validation.get_expression_field_names("context_today().strftime('%Y-%m-%d') or field"),
{'field'},
)
self.assertEqual(
view_validation.get_expression_field_names("(datetime.datetime.combine(context_today(), datetime.time(x,y,z)).to_utc()).strftime('%Y-%m-%d %H:%M:%S')"),
{'x', 'y', 'z'},
)
self.assertEqual(
view_validation.get_expression_field_names("set(field).intersection([1, 2])"),
{'field'},
)
class TestAccessRights(TransactionCaseWithUserDemo):
@common.users('demo')
def test_access(self):
# a user can not access directly a view
with self.assertRaises(AccessError):
self.env['ir.ui.view'].search([("model", '=', "res.partner"), ('type', '=', 'form')])
# but can call view_get
self.env['res.partner'].get_view(view_type='form')
# unless he does not have access to the model
with self.assertRaises(AccessError):
self.env['ir.ui.view'].get_view(view_type='form')
@common.tagged('post_install', '-at_install', '-standard', 'migration')
class TestAllViews(common.TransactionCase):
def test_views(self):
views = self.env['ir.ui.view'].with_context(lang=None).search([])
for index, view in enumerate(views):
if index % 500 == 0:
_logger.info('checked %s/%s views', index, len(views))
with self.subTest(name=view.name):
view._check_xml()
@common.tagged('post_install', '-at_install', '-standard', 'render_all_views')
class TestRenderAllViews(TransactionCaseWithUserDemo):
@common.users('demo', 'admin')
def test_render_all_views(self):
env = self.env(context={'lang': 'en_US'})
count = 0
elapsed = 0
for model in env.values():
if not model._abstract and model.check_access_rights('read', False):
with self.subTest(model=model):
times = []
for _ in range(5):
env.invalidate_all()
before = time.perf_counter()
model.get_view()
times.append(time.perf_counter() - before)
count += 1
elapsed += min(times)
_logger.info('Rendered %d views as %s using (best of 5) %ss',
count, self.env.user.name, elapsed)
class CompRegexTest(common.TransactionCase):
def test_comp_regex(self):
self.assertIsNone(re.search(ir_ui_view.COMP_REGEX, ""))
self.assertIsNone(re.search(ir_ui_view.COMP_REGEX, "__comp__2"))
self.assertIsNone(re.search(ir_ui_view.COMP_REGEX, "__comp___that"))
self.assertIsNone(re.search(ir_ui_view.COMP_REGEX, "a__comp__"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "__comp__"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "__comp__ "))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, " __comp__ "))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "__comp__.props"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "__comp__ .props"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "__comp__['props']"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "__comp__ ['props']"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "__comp__[\"props\"]"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "__comp__ [\"props\"]"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, " __comp__ [\"props\"] "))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "record ? __comp__ : false"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "!__comp__.props.resId"))
self.assertIsNotNone(re.search(ir_ui_view.COMP_REGEX, "{{ __comp__ }}"))
@common.tagged('at_install', 'modifiers')
class ViewModifiers(ViewCase):
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_01_modifiers(self):
def _test_modifiers(what, expected_vnames):
if isinstance(what, dict):
node = etree.Element('field', {k: str(v) for k, v in what.items()})
else:
node = etree.fromstring(what) if isinstance(what, str) else what
modifiers = {attr: node.attrib[attr] for attr in node.attrib if attr in ir_ui_view.VIEW_MODIFIERS}
vnames = set()
for attr, expr in modifiers.items():
vnames |= view_validation.get_expression_field_names(expr) - {'id'}
assert vnames == expected_vnames, f"{vnames!r} != {expected_vnames!r}"
str_true = "True"
_test_modifiers('<field name="a"/>', set())
_test_modifiers('<field name="a" invisible="1"/>', set())
_test_modifiers('<field name="a" readonly="1"/>', set())
_test_modifiers('<field name="a" required="1"/>', set())
_test_modifiers('<field name="a" invisible="0"/>', set())
_test_modifiers('<field name="a" readonly="0"/>', set())
_test_modifiers('<field name="a" required="0"/>', set())
# TODO: Order is not guaranteed
_test_modifiers('<field name="a" invisible="1" required="1"/>',
set(),
)
_test_modifiers('<field name="a" invisible="1" required="0"/>',
set(),
)
_test_modifiers('<field name="a" invisible="0" required="1"/>',
set(),
)
_test_modifiers("""<field name="a" invisible="b == 'c'"/>""",
{"b"},
)
_test_modifiers("""<field name="a" invisible="b == 'c'"/>""",
{"b"},
)
_test_modifiers("""<field name="a" invisible="b == 'c'"/>""",
{"b"},
)
_test_modifiers("""<field name="a" invisible="(b == 'c' or e == 'f')"/>""",
{"b", "e"},
)
_test_modifiers("""<field name="a" invisible="b == 'c'"/>""",
{"b"},
)
_test_modifiers("""<field name="a" invisible="user_id == uid"/>""",
{"user_id"},
)
_test_modifiers("""<field name="a" invisible="(user_id == other_field)"/>""",
{"user_id", "other_field"},
)
_test_modifiers("""<field name="a" invisible="a == parent.b"/>""",
{"a", "parent.b"},
)
_test_modifiers("""<field name="a" invisible="a == context.get('b')"/>""",
{"a"},
)
_test_modifiers("""<field name="a" invisible="a == context['b']"/>""",
{"a"},
)
_test_modifiers("""<field name="a" invisible="company_id == allowed_company_ids[0]"/>""",
{"company_id"},
)
_test_modifiers("""<field name="a" invisible="company_id == (field_1 or False)"/>""",
{"company_id", "field_1"},
)
# fields in a tree view
tree = etree.fromstring('''
<tree>
<header>
<button name="a" invisible="1"/>
</header>
<field name="a"/>
<field name="a" invisible="0"/>
<field name="a" column_invisible="1"/>
<field name="a" invisible="b == 'c'"/>
<field name="a" invisible="(b == 'c')"/>
</tree>
''')
_test_modifiers(tree[0][0], set())
_test_modifiers(tree[1], set())
_test_modifiers(tree[2], set())
_test_modifiers(tree[3], set())
_test_modifiers(tree[4], {"b"})
_test_modifiers(tree[5], {"b"})
# The dictionary is supposed to be the result of fields_get().
_test_modifiers({}, set())
_test_modifiers({"invisible": str_true}, set())
_test_modifiers({"invisible": False}, set())
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_03_modifier_attribute_is_boolean(self):
arch = """
<form string="View">
<field name="model"/>
<field name="name" readonly="%s"/>
</form>
"""
self.assertValid(arch % '1')
self.assertValid(arch % '0')
self.assertValid(arch % 'True')
self.assertValid(arch % "[('model', '=', '1')]")
def test_04_modifier_attribute_using_context(self):
view = self.assertValid("""
<form string="View">
<field name="name"
invisible="context.get('foo')"
readonly="context.get('bar')"
required="context.get('baz')"
/>
</form>
""")
arch = self.View.with_context(foo=True).get_view(view.id)['arch']
field_node = etree.fromstring(arch).xpath('//field[@name="name"]')[0]
self.assertEqual(field_node.get('invisible'), "context.get('foo')")
self.assertEqual(field_node.get('readonly'), "context.get('bar')")
self.assertEqual(field_node.get('required'), "context.get('baz')")
def test_05_modifier_attribute_priority(self):
view = self.assertValid("""
<form string="View">
<field name="type" invisible="1"/>
<field name="name" invisible="context.get('foo') and type == 'tree'"/>
</form>
""")
for type_value, context, expected in [
('tree', {}, False),
('form', {}, False),
('tree', {'foo': True}, True),
('form', {'foo': True}, False),
]:
arch = self.View.with_context(**context).get_view(view.id)['arch']
field_node = etree.fromstring(arch).xpath('//field[@name="name"]')[0]
result = field_node.get('invisible')
result = safe_eval.safe_eval(result, {'context': context, 'type': type_value})
self.assertEqual(bool(result), expected, f"With context: {context}")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_10_raise_for_old_attributes(self):
arch = """
<form string="View">
<field name="name"/>
<field name="model"/>
<field name="inherit_id" attrs="{'readonly': [('model', '=', 'ir.ui.view')]"/>
</form>
"""
self.assertInvalid(arch, """no longer used""")
arch = """
<form string="View">
<field name="name"/>
<field name="model"/>
<field name="inherit_id" states="draft,done"/>
</form>
"""
self.assertInvalid(arch, """no longer used""")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_11_attrs_field(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_id"
readonly="model == 'ir.ui.view'"/>
</form>
"""
self.assertValid(arch % '<field name="model"/>')
self.assertInvalid(
arch % '',
"""Field 'model' used in modifier 'readonly' (model == 'ir.ui.view') must be present in view but is missing""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_12_invalid_syntax(self):
arch = """
<form string="View">
<field name="name"/>
<field name="model"/>
<field name="inherit_id"
readonly="model 'ir.ui.view'"/>
</form>
"""
self.assertInvalid(
arch,
"""Invalid modifier 'readonly'""",
)
arch = """
<form string="View">
<field name="name"/>
<field name="model"/>
<field name="inherit_id"
readonly="bidule.get('truc') or context.get('truc')"/>
</form>
"""
self.assertInvalid(
arch,
"""Field 'bidule' used in modifier 'readonly' (bidule.get('truc') or context.get('truc')) must be present in view but is missing.""",
)
arch = """
<form string="View">
<field name="name"/>
<field name="model"/>
<field name="inherit_id"
readonly="context.get('truc') or bidule.get('toto')"/>
</form>
"""
self.assertInvalid(
arch,
"""must be present in view but is missing""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_13_attrs_states_invisible_to_modifier(self):
view = self.View.create({
'name': 'foo',
'model': 'ir.module.module',
'arch': """
<form string="View">
<group invisible="state != 'finished'">
<field name="category_id" invisible="not state" />
<field name="state" invisible="name not in ['qweb-pdf', 'qweb-html', 'qweb-text']"/>
<field name="name" invisible="name != 'bidule' and category_id != uid and state not in ('draf', 'finished')"/>
</group>
</form>
""",
})
arch = self.env['ir.module.module'].get_view(view_id=view.id)['arch']
tree = etree.fromstring(arch)
invisible = tree.xpath('//group')[0].get('invisible')
self.assertEqual(invisible, "state != 'finished'")
invisible = tree.xpath('//field[@name="category_id"]')[0].get('invisible')
self.assertEqual(invisible, "not state")
invisible = tree.xpath('//field[@name="state"]')[0].get('invisible')
self.assertEqual(invisible, "name not in ['qweb-pdf', 'qweb-html', 'qweb-text']")
invisible = tree.xpath('//field[@name="name"]')[0].get('invisible')
self.assertEqual(invisible, "name != 'bidule' and category_id != uid and state not in ('draf', 'finished')")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_14_attrs_subfield(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id"
readonly="model == 'ir.ui.view'"/>
</form>
</field>
</form>
"""
self.assertValid(arch % ('', '<field name="model"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in modifier 'readonly' (model == 'ir.ui.view') must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="model"/>', ''),
"""Field 'model' used in modifier 'readonly' (model == 'ir.ui.view') must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_15_attrs_subfield_with_parent(self):
arch = """
<form string="View">
<field name="name"/>%s
<field name="inherit_children_ids">
<form string="Children">
<field name="name"/>%s
<field name="inherit_id"
readonly="parent.model == 'ir.ui.view'"/>
</form>
</field>
</form>
"""
self.assertValid(arch % ('<field name="model"/>', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in modifier 'readonly' (parent.model == 'ir.ui.view') must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>'),
"""Field 'model' used in modifier 'readonly' (parent.model == 'ir.ui.view') must be present in view but is missing.""",
)
def test_16_attrs_groups_behavior(self):
view = self.View.create({
'name': 'foo',
'model': 'res.partner',
'arch': """
<form>
<field name="name"/>
<field name="company_id" groups="base.group_system"/>
<div id="foo"/>
<div id="bar" groups="base.group_system"/>
</form>
""",
})
user_demo = self.user_demo
# Make sure demo doesn't have the base.group_system
self.assertFalse(self.env['res.partner'].with_user(user_demo).env.user.has_group('base.group_system'))
arch = self.env['res.partner'].with_user(user_demo).get_view(view_id=view.id)['arch']
tree = etree.fromstring(arch)
self.assertTrue(tree.xpath('//field[@name="name"]'))
self.assertFalse(tree.xpath('//field[@name="company_id"]'))
self.assertTrue(tree.xpath('//div[@id="foo"]'))
self.assertFalse(tree.xpath('//div[@id="bar"]'))
user_admin = self.env.ref('base.user_admin')
# Make sure admin has the base.group_system
self.assertTrue(self.env['res.partner'].with_user(user_admin).env.user.has_group('base.group_system'))
arch = self.env['res.partner'].with_user(user_admin).get_view(view_id=view.id)['arch']
tree = etree.fromstring(arch)
self.assertTrue(tree.xpath('//field[@name="name"]'))
self.assertTrue(tree.xpath('//field[@name="company_id"]'))
self.assertTrue(tree.xpath('//div[@id="foo"]'))
self.assertTrue(tree.xpath('//div[@id="bar"]'))
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_17_attrs_groups_validation(self):
def validate(arch, valid=False, parent=False):
parent = 'parent.' if parent else ''
if valid:
self.assertValid(arch % {'attrs': f"""invisible="{parent}name == 'foo'" """})
self.assertValid(arch % {'attrs': f"""domain="[('name', '!=', {parent}name)]" """})
self.assertValid(arch % {'attrs': f"""context="{{'default_name': {parent}name}}" """})
self.assertValid(arch % {'attrs': f"""decoration-info="{parent}name == 'foo'" """})
else:
self.assertInvalid(
arch % {'attrs': f"""invisible="{parent}name == 'foo'" """},
f"""Field 'name' used in modifier 'invisible' ({parent}name == 'foo') is restricted to the group(s)""",
)
self.assertInvalid(
arch % {'attrs': f"""domain="[('name', '!=', {parent}name)]" """},
f"""Field 'name' used in domain of <field name="inherit_id"> ([('name', '!=', {parent}name)]) is restricted to the group(s)""",
)
self.assertInvalid(
arch % {'attrs': f"""context="{{'default_name': {parent}name}}" """},
f"""Field 'name' used in context ({{'default_name': {parent}name}}) is restricted to the group(s)""",
)
self.assertInvalid(
arch % {'attrs': f"""decoration-info="{parent}name == 'foo'" """},
f"""Field 'name' used in decoration-info="{parent}name == 'foo'" is restricted to the group(s)""",
)
# Assert using a field restricted to a group
# in another field without the same group is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" %(attrs)s/>
</form>
""", valid=False)
# Assert using a parent field restricted to a group
# in a child field without the same group is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" %(attrs)s/>
</tree>
</field>
</form>
""", valid=False, parent=True)
# Assert using a parent field restricted to a group
# in a child field with the same group is valid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a parent field available for everyone
# in a child field restricted to a group is valid
validate("""
<form string="View">
<field name="name"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a field available for everyone
# in another field restricted to a group is valid
validate("""
<form string="View">
<field name="name"/>
<field name="inherit_id" %(attrs)s groups="base.group_system"/>
</form>
""", valid=True)
# Assert using a field restricted to a group
# in another field with the same group is valid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field available twice for 2 diffent groups
# in another field restricted to one of the 2 groups is valid
validate("""
<form string="View">
<field name="name" groups="base.group_portal"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to a group only
# in other fields restricted to at least one different group is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
<field name="inherit_id" groups="base.group_portal" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field available twice for 2 different groups
# in other fields restricted to the same 2 group is valid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="name" groups="base.group_portal"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
<field name="inherit_id" groups="base.group_portal" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field available for 2 diffent groups,
# in another field restricted to one of the 2 groups is valid
validate("""
<form string="View">
<field name="name" groups="base.group_portal,base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field available for 1 group only
# in another field restricted 2 groups is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_portal,base.group_system" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field restricted to a group
# in another field restricted to a group including the group for which the field is available is valid
validate("""
<form string="View">
<field name="name" groups="base.group_erp_manager"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a parent field restricted to a group
# in a child field restricted to a group including the group for which the field is available is valid
validate("""
<form string="View">
<field name="name" groups="base.group_erp_manager"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a field restricted to a group
# in another field restricted to a group not including the group for which the field is available is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_erp_manager" %(attrs)s/>
</form>
""", valid=False)
# Assert using a parent field restricted to a group
# in a child field restricted to a group not including the group for which the field is available is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids">
<tree editable="bottom">
<field name="inherit_id" groups="base.group_erp_manager" %(attrs)s/>
</tree>
</field>
</form>
""", valid=False, parent=True)
# Assert using a field within a block restricted to a group
# in another field not restricted to the same group is invalid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
</group>
<field name="inherit_id" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field within a block restricted to a group
# in another field within the same block restricted to a group is valid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=True)
# Assert using a field within a block restricted to a group
# in another field within the same block restricted to a group and additional groups on the field node is valid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
<field name="inherit_id" %(attrs)s groups="base.group_multi_currency,base.group_multi_company"/>
</group>
</form>
""", valid=True)
# Assert using a field within a block restricted to a group
# in another field within a block restricted to the same group is valid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
</group>
<group groups="base.group_system">
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=True)
# Assert using a field within a block restricted to a group
# in another field within a block restricted to a group including the group for which the field is available
# is valid
validate("""
<form string="View">
<group groups="base.group_erp_manager">
<field name="name"/>
</group>
<group groups="base.group_system">
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=True)
# Assert using a field within a block restricted to a group
# in another field within a block restricted to a group not including the group for which the field is available
# is invalid
validate("""
<form string="View">
<group groups="base.group_system">
<field name="name"/>
</group>
<group groups="base.group_erp_manager">
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=False)
# Assert using a parent field restricted to a group
# in a child field under a relational field restricted to the same group is valid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids" groups="base.group_system">
<tree editable="bottom">
<field name="inherit_id" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a parent field restricted to a group
# in a child field under a relational field restricted
# to a group including the group for which the field is available is valid
validate("""
<form string="View">
<field name="name" groups="base.group_erp_manager"/>
<field name="inherit_children_ids" groups="base.group_system">
<tree editable="bottom">
<field name="inherit_id" %(attrs)s/>
</tree>
</field>
</form>
""", valid=True, parent=True)
# Assert using a parent field restricted to a group
# in a child field under a relational field restricted
# to a group not including the group for which the field is available is invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_children_ids" groups="base.group_erp_manager">
<tree editable="bottom">
<field name="inherit_id" %(attrs)s/>
</tree>
</field>
</form>
""", valid=False, parent=True)
# Assert using a field restricted to users not having a group
# in another field not restricted to any group is invalid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="inherit_id" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field not restricted to any group
# in another field restricted to users not having a group is valid
validate("""
<form string="View">
<field name="name"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to users not having multiple groups
# in another field restricted to users not having one of the group only is invalid
# e.g.
# if the user is portal, the field "name" will not be in the view
# but the field "inherit_id" where "name" is used will be in the view
# making it invalid.
validate("""
<form string="View">
<field name="name" groups="!base.group_system,!base.group_portal"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field restricted to users not having a group
# in another field restricted to users not having multiple group including the one above is valid
# e.g.
# if the user is portal, the field "name" will be in the view
# but the field "inherit_id" where "name" is used will not be in the view
# making it valid.
validate("""
<form string="View">
<field name="name" groups="!base.group_user"/>
<field name="inherit_id" groups="!base.group_user,!base.group_portal" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to a non group
# in another field for which the non group is not implied is invalid
# e.g.
# if the user is employee, the field "name" will not be in the view
# but the field "inherit_id" where "name" is used will be in the view,
# making it invalid.
validate("""
<form string="View">
<field name="name" groups="!base.group_user"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=False)
# Assert using a field restricted to a non group
# in another field restricted to a non group implied in the non group of the available field is valid
# e.g.
# if the user is employee, the field "name" will be in the view
# but the field "inherit_id", where "name" is used, will not be in the view,
# therefore making it valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="inherit_id" groups="!base.group_user" %(attrs)s/>
</form>
""", valid=True)
# Assert using a field restricted to non-admins, itself in a block restricted to employees,
# in another field restricted to a block restricted to employees
# is invalid
# e.g.
# if the user is admin, the field "name" will not be in the view
# but the field "inherit_id", where "name" is used, will be in the view,
# threfore making it invalid
validate("""
<form string="View">
<group groups="base.group_user">
<field name="name" groups="!base.group_system"/>
</group>
<group groups="base.group_user">
<field name="inherit_id" %(attrs)s/>
</group>
</form>
""", valid=False)
# Assert using a field restricted to a group
# in another field restricted the opposite group is invalid
# e.g.
# if the user is admin, the field "name" will be in the view
# but the field "inherit_id", where "name" is used, will not be in the view,
# therefore making it invalid
validate("""
<form string="View">
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=False)
# Assert having two times the same field with a mutually exclusive group
# and using that field in another field without any group is valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" %(attrs)s/>
</form>
""", valid=True)
# Assert having two times the same field with a mutually exclusive group
# and using that field in another field using the group is valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert having two times the same field with a mutually exclusive group
# and using that field in another field using the !group is valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="!base.group_system" %(attrs)s/>
</form>
""", valid=True)
# Assert having two times the same field with a mutually exclusive group
# and using that field in another field restricted to any other group is valid
validate("""
<form string="View">
<field name="name" groups="!base.group_system"/>
<field name="name" groups="base.group_system"/>
<field name="inherit_id" groups="base.group_portal" %(attrs)s/>
</form>
""", valid=True)