Files
tradon/tests/test_modelview.py
2025-12-26 13:11:43 +00:00

908 lines
30 KiB
Python
Executable File

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import unittest
from unittest.mock import patch
from lxml import etree
from trytond.model.exceptions import (
AccessButtonError, AccessError, ButtonActionException)
from trytond.model.modelview import set_visible
from trytond.pool import Pool
from trytond.pyson import Eval, PYSONDecoder, PYSONEncoder
from trytond.tests.test_tryton import activate_module, with_transaction
class ModelView(unittest.TestCase):
"Test ModelView"
@classmethod
def setUpClass(cls):
activate_module('tests')
def test_set_visible(self):
"Test loading of visible x2m fields in group"
GROUP_XML = """
<form>
<group>
<field name="OK"/>
</group>
<group expandable="1">
<field name="OK"/>
<group expandable="0">
<field name="KO"/>
</group>
</group>
<group expandable="0">
<field name="KO"/>
<group>
<field name="KO"/>
</group>
</group>
<group>
<group>
<field name="OK"/>
</group>
<group expandable="1">
<field name="OK"/>
</group>
<group expandable="0">
<field name="KO"/>
</group>
</group>
</form>
"""
group_tree = etree.fromstring(GROUP_XML)
set_visible(group_tree, {'OK', 'KO'})
self.assertEqual(
['OK', 'OK', 'OK', 'OK'],
[n.attrib['name']
for n in group_tree.xpath("//field[@loading='eager']")])
def test_mark_visible_x2m_notebook(self):
"Test marking visible x2m fields in notebook"
NOTEBOOK_XML = """
<form>
<notebook>
<page>
<field name="OK"/>
<field name="OK"/>
<notebook>
<page>
<field name="OK"/>
<notebook>
<page>
<field name="OK"/>
<field name="OK"/>
</page>
<page>
<field name="KO"/>
</page>
</notebook>
</page>
<page>
<field name="KO"/>
</page>
</notebook>
</page>
<page>
<field name="KO"/>
<notebook>
<page>
<field name="KO"/>
</page>
<page>
<field name="KO"/>
</page>
</notebook>
</page>
</notebook>
</form>
"""
nb_tree = etree.fromstring(NOTEBOOK_XML)
set_visible(nb_tree, {'OK', 'KO'})
self.assertEqual(
['OK', 'OK', 'OK', 'OK', 'OK'],
[n.attrib['name']
for n in nb_tree.xpath("//field[@loading='eager']")])
def test_mark_visible_x2m_notebook_group(self):
"Test marking visible x2m fields in notebook and groups"
NBGROUP_XML = """
<form>
<notebook>
<page>
<group>
<field name="OK"/>
</group>
<group expandable="0">
<field name="KO"/>
</group>
<group expandable="1">
<field name="OK"/>
</group>
</page>
<page>
<group>
<field name="KO"/>
</group>
<group expandable="0">
<field name="KO"/>
</group>
<group expandable="1">
<field name="KO"/>
</group>
</page>
</notebook>
<group expandable="0">
<notebook>
<page>
<field name="KO"/>
</page>
<page>
<field name="KO"/>
</page>
</notebook>
</group>
<group expandable="1">
<notebook>
<page>
<field name="OK"/>
</page>
<page>
<field name="KO"/>
</page>
</notebook>
</group>
<group>
<notebook>
<page>
<field name="OK"/>
</page>
<page>
<field name="KO"/>
</page>
</notebook>
</group>
</form>
"""
ng_tree = etree.fromstring(NBGROUP_XML)
set_visible(ng_tree, {'OK', 'KO'})
self.assertEqual(
['OK', 'OK', 'OK', 'OK'],
[n.attrib['name']
for n in ng_tree.xpath("//field[@loading='eager']")])
@with_transaction()
def test_changed_values(self):
"Test ModelView._changed_values"
pool = Pool()
Model = pool.get('test.modelview.changed_values')
Target = pool.get('test.modelview.changed_values.target')
record = Model(targets=[], m2m_targets=[], m2m_function=[])
self.assertEqual(record._changed_values, {})
record.name = 'foo'
record.target = Target(1)
record.ref_target = Target(2)
record.targets = [Target(name='bar')]
record.multiselection = ['a']
record.dictionary = {'key': 'value'}
self.assertEqual(record._changed_values, {
'name': 'foo',
'target': 1,
'ref_target': 'test.modelview.changed_values.target,2',
'targets': {
'add': [
(0, {'id': None, 'name': 'bar'}),
],
},
'multiselection': ('a',),
'dictionary': {'key': 'value'},
})
record = Model(name='test', target=1, targets=[
{'id': 1, 'name': 'foo'},
{'id': 2},
],
m2m_targets=[5, 6, 7],
multiselection=['a'],
dictionary={'key': 'value'},
)
self.assertEqual(record._changed_values, {})
target = record.targets[0]
target.name = 'bar'
record.targets = [target]
record.m2m_targets = [Target(9), Target(10)]
ms = list(record.multiselection)
ms.append('b')
record.multiselection = ms
dico = record.dictionary.copy()
dico['key'] = 'another value'
record.dictionary = dico
self.assertEqual(record._changed_values, {
'targets': {
'update': [{'id': 1, 'name': 'bar'}],
'delete': [2],
},
'm2m_targets': {
'remove': [5, 6, 7],
'add': [(0, {'id': 9}), (1, {'id': 10})],
},
'multiselection': ('a', 'b'),
'dictionary': {'key': 'another value'},
})
# change only one2many record
record = Model(targets=[{'id': 1, 'name': 'foo'}])
self.assertEqual(record._changed_values, {})
target, = record.targets
target.name = 'bar'
record.targets = record.targets
self.assertEqual(record._changed_values, {
'targets': {
'update': [{'id': 1, 'name': 'bar'}],
},
})
# no initial value
record = Model(targets=[], m2m_targets=[], m2m_function=[])
record._values = record._record()
target = Target(id=1)
record._values['targets'] = [target]
target.name = 'foo'
self.assertEqual(record._changed_values, {
'targets': {
'add': [(0, {'id': 1, 'name': 'foo'})],
},
})
@with_transaction()
def test_changed_values_2many_function(self):
"Test changed values for xxx2many function field"
pool = Pool()
Model = pool.get('test.modelview.changed_values')
with patch.object(
Model, 'on_change_with_m2m_function') as on_change_with:
on_change_with.return_value = [42]
record = Model()
record.m2m_function = record.on_change_with_m2m_function()
self.assertEqual(record._changed_values, {
'm2m_function': {'add': [(0, {'id': 42})]},
})
@with_transaction()
def test_changed_values_stored(self):
"Test stored changed values"
pool = Pool()
Model = pool.get('test.modelview.stored.changed_values')
Target = pool.get('test.modelview.stored.changed_values.target')
record = Model()
record.targets = [Target(name="foo"), Target(name="bar")]
record.save()
target1, target2 = record.targets
record = Model(record.id)
target1.name = "test"
record.targets = [target1, Target(name="baz")]
self.assertEqual(record._changed_values, {
'targets': {
'delete': [target2.id],
'update': [{'id': target1.id, 'name': "test"}],
'add': [(1, {'id': None, 'name': "baz"})],
},
})
@with_transaction()
def test_changed_values_removed(self):
"Test removed"
pool = Pool()
Model = pool.get('test.modelview.changed_values')
Target = pool.get('test.modelview.changed_values.target')
target = Target(1)
record = Model(targets=[target])
Model.targets.remove(record, [target])
self.assertEqual(record._changed_values, {
'targets': {
'remove': [1],
},
})
@with_transaction()
def test_changed_values_deleted(self):
"Test deleted"
pool = Pool()
Model = pool.get('test.modelview.changed_values')
Target = pool.get('test.modelview.changed_values.target')
target = Target(1)
record = Model(m2m_targets=[target])
Model.m2m_targets.delete(record, [target])
self.assertEqual(record._changed_values, {
'm2m_targets': {
'delete': [1],
},
})
@with_transaction()
def test_changed_values_rec_name(self):
"Test rec_name of ModelStorage is added"
pool = Pool()
Model = pool.get('test.modelview.changed_values')
Target = pool.get('test.modelview.changed_values.stored_target')
target, = Target.create([{'name': "Target"}])
record = Model()
record.stored_target = target
self.assertEqual(record._changed_values, {
'stored_target': target.id,
'stored_target.': {
'rec_name': "Target",
},
})
@with_transaction()
def test_changed_values_reverse_field(self):
"Test _changed_values with reverse field set"
pool = Pool()
Model = pool.get('test.modelview.changed_values')
Target = pool.get('test.modelview.changed_values.target')
record = Model(id=1, targets=[], m2m_targets=[], m2m_function=[])
record.name = "Record"
target = Target()
target.name = "Target"
record.targets = [target]
self.assertEqual(record._changed_values, {
'name': "Record",
'targets': {
'add': [(0, {'id': None, 'name': "Target"})],
},
})
@with_transaction(context={'_check_access': True})
def test_button_access(self):
'Test Button Access'
pool = Pool()
TestModel = pool.get('test.modelview.button')
Button = pool.get('ir.model.button')
ModelAccess = pool.get('ir.model.access')
Group = pool.get('res.group')
admin, = Group.search([('name', '=', 'Administration')])
test = TestModel()
button = Button(model=TestModel.__name__, name='test')
button.save()
# Without model/button access
TestModel.test([test])
# Without read access
access = ModelAccess(
model=TestModel.__name__, group=None, perm_read=False)
access.save()
with self.assertRaises(AccessError):
TestModel.test([test])
# Without write access
access.perm_read = True
access.perm_write = False
access.save()
with self.assertRaises(AccessError):
TestModel.test([test])
# Without write access but with button access
button.groups = [admin]
button.save()
TestModel.test([test])
# Without button access
ModelAccess.delete([access])
no_group = Group(name='no group')
no_group.save()
button.groups = [no_group]
button.save()
with self.assertRaises(AccessButtonError):
TestModel.test([test])
@with_transaction(context={'_check_access': True})
def test_button_no_rule(self):
"Test no Button Rule"
pool = Pool()
TestModel = pool.get('test.modelview.button')
ButtonClick = pool.get('ir.model.button.click')
record = TestModel(id=-1)
with patch.object(TestModel, 'test_non_decorated') as button_func:
TestModel.test([record])
button_func.assert_called_with([record])
clicks = ButtonClick.search([
('record_id', '=', record.id),
])
self.assertEqual(len(clicks), 0)
@with_transaction(context={'_check_access': True})
def test_button_rule_not_passed(self):
"Test not passed Button Rule"
pool = Pool()
TestModel = pool.get('test.modelview.button')
Button = pool.get('ir.model.button')
ButtonRule = pool.get('ir.model.button.rule')
ButtonClick = pool.get('ir.model.button.click')
rule = ButtonRule(number_user=2)
button = Button(model=TestModel.__name__, name='test', rules=[rule])
button.save()
record = TestModel(id=-1)
with patch.object(TestModel, 'test_non_decorated') as button_func:
TestModel.test([record])
button_func.assert_called_with([])
clicks = ButtonClick.search([
('button', '=', button.id),
('record_id', '=', record.id),
])
self.assertEqual(len(clicks), 1)
click, = clicks
self.assertEqual(click.user.id, 1)
@with_transaction(context={'_check_access': True})
def test_button_rule_passed(self):
"Test passed Button Rule"
pool = Pool()
TestModel = pool.get('test.modelview.button')
Button = pool.get('ir.model.button')
ButtonRule = pool.get('ir.model.button.rule')
ButtonClick = pool.get('ir.model.button.click')
rule = ButtonRule(number_user=1)
button = Button(model=TestModel.__name__, name='test', rules=[rule])
button.save()
record = TestModel(id=-1)
with patch.object(TestModel, 'test_non_decorated') as button_func:
TestModel.test([record])
button_func.assert_called_with([record])
clicks = ButtonClick.search([
('button', '=', button.id),
('record_id', '=', record.id),
])
self.assertEqual(len(clicks), 1)
click, = clicks
self.assertEqual(click.user.id, 1)
@with_transaction()
def test_button_rule_test_condition(self):
"Test condition Button Rule"
pool = Pool()
TestModel = pool.get('test.modelview.button')
Button = pool.get('ir.model.button')
ButtonRule = pool.get('ir.model.button.rule')
ButtonClick = pool.get('ir.model.button.click')
button = Button()
clicks = [ButtonClick(user=1)]
condition = PYSONEncoder().encode(
Eval('self', {}).get('value', 0) > 48)
rule = ButtonRule(
condition=condition, group=None, number_user=2, button=button)
record = TestModel(id=-1)
record.value = 10
self.assertTrue(rule.test(record, clicks))
record.value = 50
self.assertFalse(rule.test(record, clicks))
@with_transaction()
def test_button_rule_test_group(self):
"Test group Button Rule"
pool = Pool()
TestModel = pool.get('test.modelview.button')
Button = pool.get('ir.model.button')
ButtonRule = pool.get('ir.model.button.rule')
ButtonClick = pool.get('ir.model.button.click')
User = pool.get('res.user')
Group = pool.get('res.group')
group = Group()
user = User()
user.groups = []
button = Button()
clicks = [ButtonClick(user=user)]
rule = ButtonRule(
condition=None, group=group, number_user=1, button=button)
record = TestModel()
self.assertFalse(rule.test(record, clicks))
user.groups = [group]
self.assertTrue(rule.test(record, clicks))
@with_transaction()
def test_button_rule_test_number_user(self):
"Test number user Button Rule"
pool = Pool()
TestModel = pool.get('test.modelview.button')
Button = pool.get('ir.model.button')
ButtonRule = pool.get('ir.model.button.rule')
ButtonClick = pool.get('ir.model.button.click')
User = pool.get('res.user')
user1 = User()
user2 = User()
button = Button()
rule = ButtonRule(
condition=None, group=None, number_user=2, button=button)
record = TestModel()
# No click
self.assertFalse(rule.test(record, []))
# Only one click
clicks = [ButtonClick(user=user1)]
self.assertFalse(rule.test(record, clicks))
# Two clicks from the same user
clicks = [ButtonClick(user=user1), ButtonClick(user=user1)]
self.assertFalse(rule.test(record, clicks))
# Two clicks from different users
clicks = [ButtonClick(user=user1), ButtonClick(user=user2)]
self.assertTrue(rule.test(record, clicks))
@with_transaction()
def test_button_action(self):
"Test button action"
pool = Pool()
TestModel = pool.get('test.modelview.button_action')
action_id = TestModel.test([])
self.assertIsInstance(action_id, int)
@with_transaction()
def test_button_action_return(self):
"Test button action update value"
pool = Pool()
TestModel = pool.get('test.modelview.button_action')
action = TestModel.test_update([])
self.assertEqual(action['url'], 'http://www.tryton.org/')
@with_transaction()
def test_button_change(self):
"Test button change"
pool = Pool()
Model = pool.get('test.modelview.button_change')
decoder = PYSONDecoder()
view = Model.fields_view_get()
tree = etree.fromstring(view['arch'])
button = tree.xpath('//button[@name="test"]')[0]
self.assertEqual(
set(decoder.decode(button.attrib['change'])),
{'name', 'extra'})
@with_transaction()
def test_link(self):
"Test link in view"
pool = Pool()
TestModel = pool.get('test.modelview.link')
arch = TestModel.fields_view_get()['arch']
parser = etree.XMLParser()
tree = etree.fromstring(arch, parser=parser)
link, = tree.xpath('//link')
self.assertTrue(link.attrib['id'])
self.assertIsInstance(int(link.attrib['id']), int)
@with_transaction()
def test_link_without_read_access(self):
"Test link in view without read access"
pool = Pool()
TestModel = pool.get('test.modelview.link')
ModelAccess = pool.get('ir.model.access')
access = ModelAccess(
model='test.modelview.link.target', group=None, perm_read=False)
access.save()
arch = TestModel.fields_view_get()['arch']
parser = etree.XMLParser()
tree = etree.fromstring(arch, parser=parser)
links = tree.xpath('//link')
labels = tree.xpath('//label')
self.assertFalse(links)
self.assertTrue(labels)
@unittest.skipUnless(hasattr(etree, 'RelaxNG'), "etree is missing RelaxNG")
@with_transaction()
def test_link_label_valid_view(self):
"Test that replacing link by label results in a valid view"
pool = Pool()
TestModel = pool.get('test.modelview.link')
ModelAccess = pool.get('ir.model.access')
UIView = pool.get('ir.ui.view')
access = ModelAccess(
model='test.modelview.link.target', group=None, perm_read=False)
access.save()
arch = TestModel.fields_view_get()['arch']
parser = etree.XMLParser()
tree = etree.fromstring(arch, parser=parser)
validator = etree.RelaxNG(etree=UIView.get_rng('form'))
self.assertTrue(validator.validate(tree))
@with_transaction()
def test_link_without_action_access(self):
"Test link in view without action access"
pool = Pool()
TestModel = pool.get('test.modelview.link')
ActionWindow = pool.get('ir.action.act_window')
Group = pool.get('res.group')
ActionGroup = pool.get('ir.action-res.group')
group = Group(name="Group")
group.save()
action_window, = ActionWindow.search(
[('res_model', '=', 'test.modelview.link.target')])
ActionGroup(
action=action_window.action,
group=group).save()
arch = TestModel.fields_view_get()['arch']
parser = etree.XMLParser()
tree = etree.fromstring(arch, parser=parser)
links = tree.xpath('//link')
labels = tree.xpath('//label')
self.assertFalse(links)
self.assertTrue(labels)
@with_transaction()
def test_rpc_setup(self):
"Testing the computation of the RPC methods"
pool = Pool()
TestModel = pool.get('test.modelview.rpc')
def check_rpc(rpc, attributes):
for key, value in attributes.items():
self.assertEqual(getattr(rpc, key), value)
NO_INSTANTIATION = {
'instantiate': None,
}
INSTANTIATE_FIRST = {
'instantiate': 0,
}
for rpc_name, rpc_attrs in [
('get_selection', NO_INSTANTIATION),
('get_function_selection', NO_INSTANTIATION),
('get_reference', NO_INSTANTIATION),
('get_function_reference', NO_INSTANTIATION),
('on_change_with_integer', INSTANTIATE_FIRST),
('on_change_float', INSTANTIATE_FIRST),
('autocomplete_char', INSTANTIATE_FIRST),
]:
self.assertIn(rpc_name, TestModel.__rpc__)
check_rpc(TestModel.__rpc__[rpc_name], rpc_attrs)
@with_transaction()
def test_remove_empty_page(self):
"Testing the removal of empty pages"
pool = Pool()
EmptyPage = pool.get('test.modelview.empty_page')
arch = EmptyPage.fields_view_get(view_type='form')['arch']
parser = etree.XMLParser()
tree = etree.fromstring(arch, parser=parser)
pages = tree.xpath('//page')
self.assertEqual(len(pages), 1)
self.assertEqual(pages[0].attrib['id'], 'non-empty')
@with_transaction()
def test_active_field(self):
"Testing active field is set and added to view fields"
pool = Pool()
Deactivable = pool.get('test.deactivable.modelview')
EmptyPage = pool.get('test.modelview.empty_page')
fields = Deactivable.fields_view_get(view_type='tree')['fields']
self.assertIn('active', fields)
fields = EmptyPage.fields_view_get(view_type='tree')['fields']
self.assertNotIn('active', fields)
@with_transaction(context={'_check_access': True})
def test_circular_depends_removed(self):
"Testing circular depends are removed when user has no access"
pool = Pool()
CircularDepends = pool.get('test.modelview.circular_depends')
FieldAccess = pool.get('ir.model.field.access')
FieldAccess.create([{
'model': CircularDepends.__name__,
'field': 'foo',
'group': None,
'perm_read': False,
}])
fields = CircularDepends.fields_view_get(view_type='form')['fields']
self.assertEqual(fields, {})
@with_transaction()
def test_depends_depends(self):
"Test depends of depends are included"
pool = Pool()
DependsDepends = pool.get('test.modelview.depends_depends')
fields = DependsDepends.fields_view_get(view_type='form')['fields']
self.assertEqual(fields.keys(), {'id', 'foo', 'bar', 'baz'})
@with_transaction(context={'_check_access': True})
def test_button_depends_access(self):
"Testing buttons are not removed when dependant fields are accesible"
pool = Pool()
Button = pool.get('test.modelview.button_depends')
arch = Button.fields_view_get(view_type='form')['arch']
parser = etree.XMLParser()
tree = etree.fromstring(arch, parser=parser)
buttons = tree.xpath('//button')
self.assertEqual(len(buttons), 1)
@with_transaction(context={'_check_access': True})
def test_button_depends_no_access(self):
"Testing buttons are removed when dependant fields are not accesible"
pool = Pool()
Button = pool.get('test.modelview.button_depends')
FieldAccess = pool.get('ir.model.field.access')
FieldAccess.create([{
'model': Button.__name__,
'field': 'value',
'group': None,
'perm_read': False,
}])
arch = Button.fields_view_get(view_type='form')['arch']
parser = etree.XMLParser()
tree = etree.fromstring(arch, parser=parser)
buttons = tree.xpath('//button')
self.assertEqual(len(buttons), 0)
@with_transaction()
def test_view_attributes(self):
"Testing view attributes are applied on view"
pool = Pool()
ViewAttributes = pool.get('test.modelview.view_attributes')
arch = ViewAttributes.fields_view_get(view_type='form')['arch']
parser = etree.XMLParser()
tree = etree.fromstring(arch, parser=parser)
field, = tree.xpath('//field[@name="foo"]')
self.assertTrue(field.attrib.get('visual'))
@with_transaction()
def test_view_attributes_depends(self):
"Testing view attributes depends are included on fields"
pool = Pool()
ViewAttributes = pool.get('test.modelview.view_attributes_depends')
fields = ViewAttributes.fields_view_get(view_type='form')['fields']
self.assertIn('bar', fields)
@with_transaction()
def test_states_depends_display(self):
"Test that fields used in readonly states are included"
pool = Pool()
Depends = pool.get('test.modelview.states_depends')
readonly_fields = Depends.fields_view_get(view_type='tree')['fields']
self.assertIn('bar', readonly_fields)
self.assertNotIn('baz', readonly_fields)
self.assertIn('quux', readonly_fields)
@with_transaction()
def test_states_depends_edition(self):
"Test that fields used in editable states are included"
pool = Pool()
Depends = pool.get('test.modelview.states_depends')
writeable_fields = Depends.fields_view_get(view_type='form')['fields']
self.assertIn('bar', writeable_fields)
self.assertIn('baz', writeable_fields)
self.assertIn('quux', writeable_fields)
@with_transaction()
def test_button_action_exception(self):
"Test value of ButtonActionException"
exception = ButtonActionException('tests.test_modelview_button_action')
self.assertEqual(exception.value, {
**exception.value,
'name': "Test Button Action",
'url': 'http://www.example.com/',
})
@with_transaction()
def test_button_action_exception_value(self):
"Test value of ButtonActionException"
exception = ButtonActionException(
'tests.test_modelview_button_action',
value={
'url': 'https://www.example.net',
})
self.assertEqual(exception.value, {
**exception.value,
'name': "Test Button Action",
'url': 'https://www.example.net',
})
@with_transaction()
def test_autocomplete(self):
"Test autocomplete"
pool = Pool()
Model = pool.get('test.modelview.autocomplete')
self.assertEqual(Model.autocomplete('test'), [])
@with_transaction()
def test_autocomplete_storage(self):
"Test autocomplete storage"
pool = Pool()
Model = pool.get('test.modelview.autocomplete.storage')
foo, bar = Model.create([{'name': 'foo'}, {'name': 'bar'}])
self.assertEqual(Model.autocomplete('test'), [])
self.assertEqual(
Model.autocomplete('foo'),
[{'name': 'foo', 'id': foo.id, 'defaults': None}])
self.assertEqual(
Model.autocomplete(''), [
{'name': 'foo', 'id': foo.id, 'defaults': None},
{'name': 'bar', 'id': bar.id, 'defaults': None}])
self.assertEqual(Model.autocomplete('foo', [('id', '!=', foo.id)]), [])
self.assertEqual(
Model.autocomplete('', limit=1),
[{'name': 'foo', 'id': foo.id, 'defaults': None}])
self.assertEqual(
Model.autocomplete('', order=[('name', 'ASC')]), [
{'name': 'bar', 'id': bar.id, 'defaults': None},
{'name': 'foo', 'id': foo.id, 'defaults': None}])