# -*- 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='
')
v.update_field_translations('arch_db', {'fr_FR': {'Foo': 'Fou', 'Bar': 'Barre'}})
self.assertEqual(v.arch, '')
# modify v to discard translations; this should not invalidate 'arch'!
v.arch = ''
self.assertEqual(v.arch, '')
def test_get_combined_arch_query_count(self):
# If the query count increases, you probably made the view combination
# fetch an extra field on views. You better fetch that extra field with
# the query of _get_inheriting_views() and manually feed the cache.
self.env.invalidate_all()
with self.assertQueryCount(3):
# 1: browse([self.view_ids['A']])
# 2: _get_inheriting_views: id, inherit_id, mode, groups
# 3: _combine: arch_db
self.view_ids['A'].get_combined_arch()
def test_view_validate_button_action_query_count(self):
_, _, counter = get_cache_key_counter(self.env['ir.model.data']._xmlid_lookup, 'base.action_ui_view')
hit, miss = counter.hit, counter.miss
with self.assertQueryCount(7):
base_view = self.assertValid("""
""")
self.assertEqual(counter.hit, hit)
self.assertEqual(counter.miss, miss + 2)
with self.assertQueryCount(6):
self.assertValid("""
""", inherit_id=base_view.id)
self.assertEqual(counter.hit, hit + 2)
self.assertEqual(counter.miss, miss + 2)
def test_view_validate_attrs_groups_query_count(self):
_, _, counter = get_cache_key_counter(self.env['ir.model.data']._xmlid_lookup, 'base.group_system')
hit, miss = counter.hit, counter.miss
with self.assertQueryCount(4):
base_view = self.assertValid("""
""")
self.assertEqual(counter.hit, hit)
self.assertEqual(counter.miss, miss + 1)
with self.assertQueryCount(4):
self.assertValid("""
""", inherit_id=base_view.id)
self.assertEqual(counter.hit, hit + 1)
self.assertEqual(counter.miss, miss + 1)
class TestApplyInheritanceSpecs(ViewCase):
""" Applies a sequence of inheritance specification nodes to a base
architecture. IO state parameters (cr, uid, model, context) are used for
error reporting
The base architecture is altered in-place.
"""
def setUp(self):
super(TestApplyInheritanceSpecs, self).setUp()
self.base_arch = E.form(
E.field(name="target"),
string="Title")
self.adv_arch = E.form(
E.field(
"TEXT1",
E.field(name="subtarget"),
"TEXT2",
E.field(name="anothersubtarget"),
"TEXT3",
name="target",
),
string="Title")
def test_replace_outer(self):
spec = E.field(
E.field(name="replacement"),
name="target", position="replace")
self.View.apply_inheritance_specs(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.form(E.field(name="replacement"), string="Title"))
def test_delete(self):
spec = E.field(name="target", position="replace")
self.View.apply_inheritance_specs(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.form(string="Title"))
def test_insert_after(self):
spec = E.field(
E.field(name="inserted"),
name="target", position="after")
self.View.apply_inheritance_specs(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.form(
E.field(name="target"),
E.field(name="inserted"),
string="Title"
))
def test_insert_before(self):
spec = E.field(
E.field(name="inserted"),
name="target", position="before")
self.View.apply_inheritance_specs(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.form(
E.field(name="inserted"),
E.field(name="target"),
string="Title"))
def test_insert_inside(self):
default = E.field(E.field(name="inserted"), name="target")
spec = E.field(E.field(name="inserted 2"), name="target", position='inside')
self.View.apply_inheritance_specs(self.base_arch, default)
self.View.apply_inheritance_specs(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.form(
E.field(
E.field(name="inserted"),
E.field(name="inserted 2"),
name="target"),
string="Title"))
def test_replace_inner(self):
spec = E.field(
"TEXT 4",
E.field(name="replacement"),
"TEXT 5",
E.field(name="replacement2"),
"TEXT 6",
name="target", position="replace", mode="inner")
expected = E.form(
E.field(
"TEXT 4",
E.field(name="replacement"),
"TEXT 5",
E.field(name="replacement2"),
"TEXT 6",
name="target"),
string="Title")
# applying spec to both base_arch and adv_arch is expected to give the same result
self.View.apply_inheritance_specs(self.base_arch, spec)
self.assertEqual(self.base_arch, expected)
self.View.apply_inheritance_specs(self.adv_arch, spec)
self.assertEqual(self.adv_arch, expected)
def test_unpack_data(self):
spec = E.data(
E.field(E.field(name="inserted 0"), name="target"),
E.field(E.field(name="inserted 1"), name="target"),
E.field(E.field(name="inserted 2"), name="target"),
E.field(E.field(name="inserted 3"), name="target"),
)
self.View.apply_inheritance_specs(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.form(
E.field(
E.field(name="inserted 0"),
E.field(name="inserted 1"),
E.field(name="inserted 2"),
E.field(name="inserted 3"),
name="target"),
string="Title"))
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_invalid_position(self):
spec = E.field(
E.field(name="whoops"),
name="target", position="serious_series")
with self.assertRaises(ValueError):
self.View.apply_inheritance_specs(self.base_arch, spec)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_incorrect_version(self):
# Version ignored on //field elements, so use something else
arch = E.form(E.element(foo="42"))
spec = E.element(
E.field(name="placeholder"),
foo="42", version="7.0")
with self.assertRaises(ValueError):
self.View.apply_inheritance_specs(arch, spec)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_target_not_found(self):
spec = E.field(name="targut")
with self.assertRaises(ValueError):
self.View.apply_inheritance_specs(self.base_arch, spec)
class TestApplyInheritanceWrapSpecs(ViewCase):
def setUp(self):
super(TestApplyInheritanceWrapSpecs, self).setUp()
self.base_arch = E.template(E.div(E.p("Content")))
def apply_spec(self, spec):
self.View.apply_inheritance_specs(self.base_arch, spec)
def test_replace(self):
spec = E.xpath(
E.div("$0", {'class': "some"}),
expr="//p", position="replace")
self.apply_spec(spec)
self.assertEqual(
self.base_arch,
E.template(E.div(
E.div(E.p('Content'), {'class': 'some'})
))
)
class TestApplyInheritanceMoveSpecs(ViewCase):
def setUp(self):
super(TestApplyInheritanceMoveSpecs, self).setUp()
self.base_arch = E.template(
E.div(E.p("Content", {'class': 'some'})),
E.div({'class': 'target'})
)
self.wrapped_arch = E.template(
E.div("aaaa", E.p("Content", {'class': 'some'}), "bbbb"),
E.div({'class': 'target'})
)
def apply_spec(self, arch, spec):
self.View.apply_inheritance_specs(arch, spec)
def test_move_replace(self):
spec = E.xpath(
E.xpath(expr="//p", position="move"),
expr="//div[@class='target']", position="replace")
self.apply_spec(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.template(
E.div(),
E.p("Content", {'class': 'some'})
)
)
self.apply_spec(self.wrapped_arch, spec)
self.assertEqual(
self.wrapped_arch,
E.template(
E.div("aaaabbbb"),
E.p("Content", {'class': 'some'})
)
)
def test_move_inside(self):
spec = E.xpath(
E.xpath(expr="//p", position="move"),
expr="//div[@class='target']", position="inside")
self.apply_spec(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.template(
E.div(),
E.div(E.p("Content", {'class': 'some'}), {'class': 'target'})
)
)
self.apply_spec(self.wrapped_arch, spec)
self.assertEqual(
self.wrapped_arch,
E.template(
E.div("aaaabbbb"),
E.div(E.p("Content", {'class': 'some'}), {'class': 'target'})
)
)
def test_move_before(self):
spec = E.xpath(
E.xpath(expr="//p", position="move"),
expr="//div[@class='target']", position="before")
self.apply_spec(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.template(
E.div(""),
E.p("Content", {'class': 'some'}),
E.div({'class': 'target'}),
)
)
self.apply_spec(self.wrapped_arch, spec)
self.assertEqual(
self.wrapped_arch,
E.template(
E.div("aaaabbbb"),
E.p("Content", {'class': 'some'}),
E.div({'class': 'target'}),
)
)
def test_move_after(self):
spec = E.xpath(
E.xpath(expr="//p", position="move"),
expr="//div[@class='target']", position="after")
self.apply_spec(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.template(
E.div(),
E.div({'class': 'target'}),
E.p("Content", {'class': 'some'}),
)
)
self.apply_spec(self.wrapped_arch, spec)
self.assertEqual(
self.wrapped_arch,
E.template(
E.div("aaaabbbb"),
E.div({'class': 'target'}),
E.p("Content", {'class': 'some'}),
)
)
def test_move_with_other_1(self):
# multiple elements with move in first position
spec = E.xpath(
E.xpath(expr="//p", position="move"),
E.p("Content2", {'class': 'new_p'}),
expr="//div[@class='target']", position="after")
self.apply_spec(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.template(
E.div(),
E.div({'class': 'target'}),
E.p("Content", {'class': 'some'}),
E.p("Content2", {'class': 'new_p'}),
)
)
def test_move_with_other_2(self):
# multiple elements with move in last position
spec = E.xpath(
E.p("Content2", {'class': 'new_p'}),
E.xpath(expr="//p", position="move"),
expr="//div[@class='target']", position="after")
self.apply_spec(self.wrapped_arch, spec)
self.assertEqual(
self.wrapped_arch,
E.template(
E.div("aaaabbbb"),
E.div({'class': 'target'}),
E.p("Content2", {'class': 'new_p'}),
E.p("Content", {'class': 'some'}),
)
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_incorrect_move_1(self):
# cannot move an inexisting element
spec = E.xpath(
E.xpath(expr="//p[@name='none']", position="move"),
expr="//div[@class='target']", position="after")
with self.assertRaises(ValueError):
self.apply_spec(self.base_arch, spec)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_incorrect_move_2(self):
# move xpath cannot contain any children
spec = E.xpath(
E.xpath(E.p("Content2", {'class': 'new_p'}), expr="//p", position="move"),
expr="//div[@class='target']", position="after")
with self.assertRaises(ValueError):
self.apply_spec(self.base_arch, spec)
def test_incorrect_move_3(self):
# move won't be correctly applied if not a direct child of an xpath
spec = E.xpath(
E.div(E.xpath(E.p("Content2", {'class': 'new_p'}), expr="//p", position="move"), {'class': 'wrapper'}),
expr="//div[@class='target']", position="after")
self.apply_spec(self.base_arch, spec)
self.assertEqual(
self.base_arch,
E.template(
E.div(E.p("Content", {'class': 'some'})),
E.div({'class': 'target'}),
E.div(E.xpath(E.p("Content2", {'class': 'new_p'}), expr="//p", position="move"), {'class': 'wrapper'}),
)
)
class TestApplyInheritedArchs(ViewCase):
""" Applies a sequence of modificator archs to a base view
"""
class TestNoModel(ViewCase):
def test_create_view_nomodel(self):
view = self.View.create({
'name': 'dummy',
'arch': '',
'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': '',
'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 = '%s'
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': """
"""
})
view2 = self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
"""
})
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': """
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """Is a ghettoWonder when I'll find paradise
"""
})
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': """
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """Is a ghettoWonder when I'll find paradise
"""
})
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
# elements from the start
'arch': """
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
"""
})
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': """
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
"""
})
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
# 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': """
"""
})
view2 = self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
"""
})
self.View.create({ # Inherit from the child view and target the added element
'name': "Extension",
'type': 'qweb',
'inherit_id': view2.id,
'arch': """
"""
})
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': """
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
"""
})
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': """
"""
})
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': """
"""
})
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': """
"""
})
self.View.create({ # Inherit from the child view and target the added element
'name': "Extension",
'type': 'qweb',
'inherit_id': view2.id,
'arch': """
"""
})
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': """
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
"""
})
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 , 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 , 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 should have been removed")
self.assertEqual(
len(body),
0,
"The processing instruction of the should have been removed")
def test_branding_inherit_top_t_field(self):
view1 = self.View.create({
'name': "Base view",
'type': 'qweb',
'arch': """
"""
})
self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
"""
})
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': """
"""
})
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': """
"""
})
view2 = self.View.create({
'name': "Extension",
'type': 'qweb',
'mode': 'primary',
'inherit_id': view1.id,
'arch': """
"""
})
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': """"""
})
view2 = self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """bar"""
})
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': """""",
})
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': """""",
})
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': """""",
})
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': """"""
})
view2 = self.View.create({
'name': "Extension",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """bar"""
})
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': """
""",
})
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': """
""",
})
self.assertEqual(view.type, 'form')
with self.assertRaises(ValidationError):
self.View.create({
'name': 'foo',
'model': 'ir.ui.view',
'type': 'form',
'arch': """
""",
})
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="""
""",
)
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="""
""",
)
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="""
""",
)
self.assertTrue(validate()) # inherited view
def test_view_inheritance(self):
view1 = self.View.create({
'name': "bob",
'model': 'ir.ui.view',
'arch': """
"""
})
view2 = self.View.create({
'name': "edmund",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': """
"""
})
view3 = self.View.create({
'name': 'jake',
'model': 'ir.ui.menu',
'inherit_id': view1.id,
'priority': 17,
'arch': """
"""
})
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("""
""", 'Field "not_a_field" does not exist in model "ir.ui.view"')
self.assertInvalid("""
""", 'Field tag must have a "name" attribute defined')
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_invalid_subfield(self):
arch = """
"""
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 = """
"""
self.assertValid(arch % '')
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 = """
"""
self.assertValid(arch % ('', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in context ({'stuff': model}) must be present in view but is missing.""",
)
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_with_parent(self):
arch = """
%s
%s
"""
self.assertValid(arch % ('', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in context ({'stuff': parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', ''),
"""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 = """
%s
%s
%s
"""
self.assertValid(arch % ('', '', ''))
self.assertInvalid(
arch % ('', '', ''),
"""Field 'model' used in context ({'stuff': parent.parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '', ''),
"""Field 'model' used in context ({'stuff': parent.parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '', ''),
"""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("""
"""
self.assertValid(arch % ('', '1', '1'))
self.assertValid(arch % ('', '0', '1'))
# self.assertInvalid(arch % ('', '1', '0'))
self.assertValid(arch % ('', '1', '0 if name else 1'))
# self.assertInvalid(arch % ('', "'tata' if name else 'tutu'", 'type'), 'xxxx')
self.assertInvalid(
arch % ('', '1', '0 if name else 1'),
"""Field 'name' used in domain of ([(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 = """
%s
"""
self.assertValid(arch % '')
self.assertInvalid(
arch % '',
"""Field 'model' used in domain of ([('model', '=', model)]) must be present in view but is missing.""",
)
def test_domain_unknown_field(self):
self.assertInvalid("""
""",
'''Unknown field "ir.ui.view.invalid_field" in domain of ([('invalid_field', '=', 'res.users')])''',
)
def test_domain_field_searchable(self):
arch = """
"""
# 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 ([('xml_id', '=', 'test')])''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_field_no_comodel(self):
self.assertInvalid("""
""", "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 = """
%s
%s
"""
self.assertValid(arch % ('', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in domain of ([('model', '=', model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in domain of ([('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 = """
%s
%s
%s
"""
self.assertValid(arch % ('', '', ''))
self.assertValid(arch % ('', '', ''))
self.assertInvalid(
arch % ('', '', ''),
"""Field 'model' used in domain of ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '', ''),
"""Field 'model' used in domain of ([('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 = """
%s
"""
self.assertValid(arch % '')
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 = """
%s
%s
"""
self.assertValid(arch % ('', ''))
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 '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 = """
%s
%s
"""
self.assertValid(arch % ('', ''))
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 '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 = """
"""
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 = """
"""
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 = """
"""
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 ([('invalid_field', '=', 'dummy')])""",
)
self.assertInvalid(
arch % ('name', 'inherit_children_ids.invalid_field'),
"""Unknown field "ir.ui.view.invalid_field" in domain of ([('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 = """
"""
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(
"""
""",
'''Invalid domain of : "['name', '=', 'dummy']"''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_searchpanel(self):
arch = """
%s
%s
"""
self.assertValid(arch % ('', '', 'view_access', 'inherit_id'))
self.assertInvalid(
arch % ('', '', 'view_access', 'inherit_id'),
"""Field 'inherit_id' used in domain of ([('view_access', '=', inherit_id)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '', 'view_access', 'view_access'),
"""Field 'view_access' used in domain of ([('view_access', '=', view_access)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '', 'inherit_id', 'inherit_id'),
"""Unknown field "res.groups.inherit_id" in domain of ([('inherit_id', '=', inherit_id)])""",
)
self.assertInvalid(
arch % ('', '', 'view_access', 'inherit_id'),
"""Field 'inherit_id' used in domain of ([('view_access', '=', inherit_id)]) is present in view but is in select multi.""",
)
arch = """
"""
self.assertInvalid(arch, "Search tag can only contain one search panel")
def test_groups_field(self):
arch = """
""",
})
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 ([('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("""
""", valid=False)
# Assert using a parent field restricted to a group
# in a child field without the same group is invalid
validate("""
""", valid=False, parent=True)
# Assert using a parent field restricted to a group
# in a child field with the same group is valid
validate("""
""", valid=True, parent=True)
# Assert using a parent field available for everyone
# in a child field restricted to a group is valid
validate("""
""", valid=True, parent=True)
# Assert using a field available for everyone
# in another field restricted to a group is valid
validate("""
""", valid=True)
# Assert using a field restricted to a group
# in another field with the same group is valid
validate("""
""", 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("""
""", valid=True)
# Assert using a field restricted to a group only
# in other fields restricted to at least one different group is invalid
validate("""
""", valid=False)
# Assert using a field available twice for 2 different groups
# in other fields restricted to the same 2 group is valid
validate("""
""", valid=True)
# Assert using a field available for 2 diffent groups,
# in another field restricted to one of the 2 groups is valid
validate("""
""", valid=True)
# Assert using a field available for 1 group only
# in another field restricted 2 groups is invalid
validate("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", valid=False)
# Assert using a field not restricted to any group
# in another field restricted to users not having a group is valid
validate("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", 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("""
""", valid=False)
validate("""
""", 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': """
""",
})
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': """
"""
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 = """
"""
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('
', 'Button must have a name')
self.assertInvalid('
', "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 = """
%s
"""
self.assertValid(arch % '')
self.assertInvalid(arch % '', "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 = """
"""
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 = """
%s
%s
"""
self.assertValid(arch % ('', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'noupdate' used in modifier 'invisible' (noupdate) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', ''),
'''Field "noupdate" does not exist in model "ir.ui.view"''',
)
self.assertInvalid(
arch % ('', ''),
'''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': """
""",
})
view2 = self.View.create({
'name': 'valid _check_xml',
'model': 'ir.ui.view',
'inherit_id': view1.id,
'active': False,
'arch': """
"""
})
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': """
bar
""",
})
def test_for_in_label(self):
self.assertValid('
')
self.assertInvalid(
'
',
"""Label tag must contain a "for". To match label style without corresponding field or button, use 'class="o_form_label"'""",
)
self.assertInvalid(
'