# -*- 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 collections import defaultdict
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 mute_logger, view_validation, safe_eval
from odoo.tools.cache import get_cache_key_counter
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, model='ir.ui.view'):
return self.View.create({
'name': name,
'model': model,
'inherit_id': inherit_id,
'arch': arch,
})
def assertInvalid(self, arch, expected_message=None, name='invalid view', inherit_id=False, model='ir.ui.view'):
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': model,
'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', model='ir.ui.view'):
with self.assertLogs('odoo.addons.base.models.ir_ui_view', level="WARNING") as log_catcher:
self.View.create({
'name': name,
'model': model,
'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", 'list'))
self.makeView('B1', self.b.id, arch=self.arch_for("B1", 'list', parent=self.b))
self.c = self.makeView('C', arch=self.arch_for("C", 'list'))
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_list = self.View.default_view(model=self.model, view_type='list')
self.assertEqual(default_list, 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(11):
base_view = self.assertValid("""
""")
self.assertEqual(counter.hit, hit)
self.assertEqual(counter.miss, miss + 2)
with self.assertQueryCount(10):
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(8):
base_view = self.assertValid("""
""")
self.assertEqual(counter.hit, hit)
self.assertEqual(counter.miss, miss)
with self.assertQueryCount(8):
self.assertValid("""
""", inherit_id=base_view.id)
self.assertEqual(counter.hit, hit)
self.assertEqual(counter.miss, miss)
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'}),
)
)
def test_move_with_tail(self):
moved_paragraph_xpath = E.xpath(expr="//p", position="move")
moved_paragraph_xpath.tail = "tail of paragraph"
spec = E.xpath(
E.p("Content2", {'class': 'new_p'}),
moved_paragraph_xpath,
expr="//div[@class='target']", position="after")
self.apply_spec(self.wrapped_arch, spec)
moved_paragraph = E.p("Content", {'class': 'some'})
moved_paragraph.tail = "tail of paragraph"
self.assertEqual(
self.wrapped_arch,
E.template(
E.div("aaaabbbb"),
E.div({'class': 'target'}),
E.p("Content2", {'class': 'new_p'}),
moved_paragraph,
)
)
@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 in (None, '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': """
""",
'''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')])''',
)
def test_domain_field_no_comodel(self):
self.assertInvalid("""
""", "Domain on non-relational field \"name\" makes no sense (domain:[('test', '=', 'test')])")
def test_domain_in_subview(self):
arch = """
""",
})
user_demo = self.user_demo
# Make sure demo doesn't have the base.group_system
self.assertFalse(user_demo.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(user_admin.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, field='name', model='ir.ui.view'):
parent = 'parent.' if parent else ''
if valid:
self.assertValid(arch % {'attrs': f"""invisible="{parent}{field} == 'foo'" """}, model=model)
self.assertValid(arch % {'attrs': f"""domain="[('name', '!=', {parent}{field})]" """}, model=model)
self.assertValid(arch % {'attrs': f"""context="{{'default_name': {parent}{field}}}" """}, model=model)
self.assertValid(arch % {'attrs': f"""decoration-info="{parent}{field} == 'foo'" """}, model=model)
else:
self.assertInvalid(
arch % {'attrs': f"""invisible="{parent}{field} == 'foo'" """},
f"""Field '{field}' used in modifier 'invisible' ({parent}{field} == 'foo') is restricted to the group(s)""",
model=model,
)
target = 'inherit_id' if model == 'ir.ui.view' else 'company_id'
self.assertInvalid(
arch % {'attrs': f"""domain="[('name', '!=', {parent}{field})]" """},
f"""Field '{field}' used in domain of ([('name', '!=', {parent}{field})]) is restricted to the group(s)""",
model=model,
)
self.assertInvalid(
arch % {'attrs': f"""context="{{'default_name': {parent}{field}}}" """},
f"""Field '{field}' used in context ({{'default_name': {parent}{field}}}) is restricted to the group(s)""",
model=model,
)
self.assertInvalid(
arch % {'attrs': f"""decoration-info="{parent}{field} == 'foo'" """},
f"""Field '{field}' used in decoration-info="{parent}{field} == 'foo'" is restricted to the group(s)""",
model=model,
)
# 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 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 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 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 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 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 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 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 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)
# The modifier node should have the same group 'base.group_user'
# (or a depending group '') that the used field 'access_token'
validate("""
""", model='ir.attachment', field='access_token', valid=True)
# 'access_token' has 'group_user' groups but only 'group_user' has access to read 'ir.attachment'
validate("""
""", field='display_name', demo=True)
# field already exist with implied groups
validate("""
""", field='name', no_add=True)
# add missing field without group because the view is already restricted to the group 'base.group_user'
validate("""
""", field='name', demo=True)
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(self.env.registry['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")
def test_tree(self):
arch = """
%s
"""
self.assertValid(arch % '')
self.assertInvalid(arch % '', "List child can only have one of field, button, control, groupby, widget, header tag (not group)")
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")
def test_tree_groupby_many2one(self):
arch = """
%s
%s
"""
view = self.assertValid(arch % ('', ''))
view_arch = view.get_views([(view.id, 'form')])['views']['form']['arch']
self.assertFalse(etree.fromstring(view_arch).xpath('//field[@name="noupdate"][@invisible][@readonly]'))
view = self.assertValid(arch % ('', ''))
view_arch = view.get_views([(view.id, 'form')])['views']['form']['arch']
self.assertTrue(etree.fromstring(view_arch).xpath('//groupby/field[@name="noupdate"][@invisible][@readonly]'))
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"''',
)
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(
'