import json import odoo from odoo import api from odoo.tools import DotDict from odoo.http import _request_stack from odoo.tests.common import TransactionCase from odoo.addons.web_studio.controllers.main import WebStudioController from copy import deepcopy from lxml import etree class TestStudioController(TransactionCase): def setUp(self): super().setUp() self.env = api.Environment(self.cr, odoo.SUPERUSER_ID, {'load_all_views': True}) _request_stack.push(self) self.session = DotDict({'debug': ''}) self.studio_controller = WebStudioController() def tearDown(self): super().tearDown() _request_stack.pop() def _transform_arch_for_assert(self, arch_string): parser = etree.XMLParser(remove_blank_text=True) arch_string = etree.fromstring(arch_string, parser=parser) return etree.tostring(arch_string, pretty_print=True, encoding='unicode') def assertViewArchEqual(self, original, expected): if original: original = self._transform_arch_for_assert(original) if expected: expected = self._transform_arch_for_assert(expected) self.assertEqual(original, expected) def update_context(self, **overrides): self.env = self.env(context=dict(self.env.context, **overrides)) class TestEditView(TestStudioController): def edit_view(self, base_view, studio_arch="", operations=None, model=None, context=None): _ops = None if isinstance(operations, list): _ops = [] for op in operations: _ops.append(deepcopy(op)) # the edit view controller may alter objects in place if studio_arch == "": studio_arch = "" return self.studio_controller.edit_view(base_view.id, studio_arch, _ops, model, context) def test_edit_view_binary_and_attribute(self): base_view = self.env['ir.ui.view'].create({ 'name': 'TestForm', 'type': 'form', 'model': 'res.partner', 'arch': """
""" }) add_binary_op = { 'type': 'add', 'target': {'tag': 'field', 'attrs': {'name': 'display_name'}, 'xpath_info': [{'tag': 'form', 'indice': 1}, {'tag': 'field', 'indice': 1}]}, 'position': 'after', 'node': {'tag': 'field', 'attrs': {}, 'field_description': {'type': 'binary', 'field_description': 'New File', 'name': 'x_studio_binary_field_WocAO', 'model_name': 'res.partner'}} } self.edit_view(base_view, operations=[add_binary_op]) self.assertViewArchEqual( base_view.get_combined_arch(), """
""" ) add_widget_op = { 'type': 'attributes', 'target': {'tag': 'field', 'attrs': {'name': 'x_studio_binary_field_WocAO'}, 'xpath_info': [{'tag': 'form', 'indice': 1}, {'tag': 'field', 'indice': 2}]}, 'position': 'attributes', 'node': {'tag': 'field', 'attrs': {'filename': 'x_studio_binary_field_WocAO_filename', 'name': 'x_studio_binary_field_WocAO', 'id': 'x_studio_binary_field_WocAO'}, 'children': [], 'has_label': True}, 'new_attrs': {'widget': 'pdf_viewer', 'options': ''} } ops = [ add_binary_op, add_widget_op ] self.edit_view(base_view, operations=ops) self.assertViewArchEqual( base_view.get_combined_arch(), """
""" ) def test_edit_view_binary_and_attribute_then_remove_binary(self): base_view = self.env['ir.ui.view'].create({ 'name': 'TestForm', 'type': 'form', 'model': 'res.partner', 'arch': """
""" }) add_binary_op = { 'type': 'add', 'target': {'tag': 'field', 'attrs': {'name': 'display_name'}, 'xpath_info': [{'tag': 'form', 'indice': 1}, {'tag': 'field', 'indice': 1}]}, 'position': 'after', 'node': {'tag': 'field', 'attrs': {}, 'field_description': {'type': 'binary', 'field_description': 'New File', 'name': 'x_studio_binary_field_WocAO', 'model_name': 'res.partner'}} } self.edit_view(base_view, operations=[add_binary_op]) add_widget_op = { 'type': 'attributes', 'target': {'tag': 'field', 'attrs': {'name': 'x_studio_binary_field_WocAO'}, 'xpath_info': [{'tag': 'form', 'indice': 1}, {'tag': 'field', 'indice': 2}]}, 'position': 'attributes', 'node': {'tag': 'field', 'attrs': {'filename': 'x_studio_binary_field_WocAO_filename', 'name': 'x_studio_binary_field_WocAO', 'id': 'x_studio_binary_field_WocAO'}, 'children': [], 'has_label': True}, 'new_attrs': {'widget': 'pdf_viewer', 'options': ''} } ops = [ add_binary_op, add_widget_op ] self.edit_view(base_view, operations=ops) remove_binary_op = { 'type': 'remove', 'target': {'tag': 'field', 'attrs': {'name': 'x_studio_binary_field_WocAO'}, 'xpath_info': [{'tag': 'form', 'indice': 1}, {'tag': 'field', 'indice': 2}]}, } self.edit_view(base_view, operations=ops + [remove_binary_op]) # The filename field is still present in the view # this is not intentional rather, it is way easier to leave this invisible field there self.assertViewArchEqual( base_view.get_combined_arch(), """
""" ) def test_edit_view_options_attribute(self): op = { 'type': 'attributes', 'target': { 'tag': 'field', 'attrs': {'name': 'groups_id'}, 'xpath_info': [ {'tag': 'group', 'indice': 1}, {'tag': 'group', 'indice': 2}, {'tag': 'field', 'indice': 2} ], 'subview_xpath': "//field[@name='user_ids']/form" }, 'position': 'attributes', 'node': { 'tag': 'field', 'attrs': { 'name': 'groups_id', 'widget': 'many2many_tags', 'options': "{'color_field': 'color'}", }, 'children': [], 'has_label': True }, 'new_attrs': {'options': '{"color_field":"color","no_create":true}'} } base_view = self.env['ir.ui.view'].create({ 'name': 'TestForm', 'type': 'form', 'model': 'res.partner', 'arch': """
""" }) self.edit_view(base_view, operations=[op], model='res.users') self.assertViewArchEqual( base_view.get_combined_arch(), """
""" ) def test_edit_view_create_attribute_attribute(self): op = { 'type': 'attributes', 'target': { 'tag': 'kanban', 'attrs': {}, 'xpath_info': [ {'tag': 'kanban', 'indice': 1}, ], 'isSubviewAttr': True, }, 'position': 'attributes', 'new_attrs': {'create': True} } base_view = self.env['ir.ui.view'].create({ 'name': 'TestKanban', 'type': 'kanban', 'model': 'res.partner', 'arch': """ """ }) self.edit_view(base_view, operations=[op], model='res.users') self.assertViewArchEqual( base_view.get_combined_arch(), """ """ ) def test_edit_view_add_binary_field_inside_group(self): arch = """
""" base_view = self.env['ir.ui.view'].create({ 'name': 'TestForm', 'type': 'form', 'model': 'res.partner', 'arch': arch }) operation = { 'type': 'add', 'target': { 'tag': 'group', 'attrs': { 'name': 'group_left' }, 'xpath_info': [ {'tag': 'form', 'indice': 1}, {'tag': 'sheet', 'indice': 1}, {'tag': 'notebook', 'indice': 1}, {'tag': 'page', 'indice': 1}, {'tag': 'group', 'indice': 1}, {'tag': 'group', 'indice': 1} ] }, 'position': 'inside', 'node': { 'tag': 'field', 'attrs': {}, 'field_description': { 'type': 'binary', 'field_description': 'New File', 'name': 'x_studio_field_fDthx', 'model_name': 'res.partner' } } } self.edit_view(base_view, operations=[operation]) expected_arch = """
""" self.assertViewArchEqual(base_view.get_combined_arch(), expected_arch) def test_edit_attribute_studio_groups(self): """ Tests the behavior of setting the attribute `studio_groups` on field view nodes having a `groups=` attribute A second goal is to test the behavior of a field node having a `groups=` attribute set on the node and another `groups=` on the field definition in the model. e.g. `code = fields.Text(string='Python Code', groups='base.group_system',` `` For this above case, a temporary technical node is created during the view postprocessing, wrapping the ` """ }) with self.debug_mode(): arch = self.env['ir.actions.server'].with_context(studio=True).get_view(view.id)['arch'] tree = etree.fromstring(arch) children = list(tree.iterdescendants()) self.assertEqual(len(children), 3, 'The tree view must have only 3 descendants in total, the 3 fields') name, state, code = children self.assertFalse(name.get('studio_groups')) self.assertTrue(state.get('studio_groups')) self.assertTrue(code.get('studio_groups')) for node in (state, code): self.assertEqual(json.loads(node.get('studio_groups'))[0]['name'], 'Technical Features') def test_edit_field_present_in_multiple_views(self): """ a use case where the hack before this fix doesn't work. We try to edit a field that is present in two views, and studio must modify the field in the correct view and do not confuse it with the other one. """ IrModelFields = self.env["ir.model.fields"].with_context(studio=True) source_model = self.env["ir.model"].search([("model", "=", "res.partner")]) destination_model = self.env["ir.model"].search( [("model", "=", "res.currency")] ) IrModelFields.create( { "ttype": "many2many", "model_id": source_model.id, "relation": destination_model.model, "name": "x_test_field_x", "relation_table": IrModelFields._get_next_relation( source_model.model, destination_model.model ), } ) arch = """
""" base_view = self.env['ir.ui.view'].create({ 'name': 'TestForm', 'type': 'form', 'model': 'res.partner', 'arch': arch }) operation = { 'type': 'attributes', 'target': { 'tag': 'field', 'attrs': { 'name': 'x_test_field_x' }, 'xpath_info': [ {'tag': 'tree', 'indice': 1}, {'tag': 'field', 'indice': 1} ], 'subview_xpath': "//field[@name='user_ids']/tree" }, 'position': 'attributes', 'node': { 'tag': 'field', 'attrs': { 'name': 'x_test_field_x', 'id': 'x_test_field_x' }, }, 'new_attrs': { 'options': "{\"no_create\": true}" } } self.edit_view(base_view, operations=[operation]) expected_arch = """
""" self.assertViewArchEqual(base_view.get_combined_arch(), expected_arch) def test_edit_attribute_studio_groups_tree_column_invisible(self): for view_type, arch, expected_modifiers in [ ('tree', """ """, {'column_invisible': 'True', 'invisible': None}), ('tree', """
""", {'invisible': 'True', 'column_invisible': None}), ('form', """
""", {'column_invisible': 'True', 'invisible': None}), ('tree', """
""", {'invisible': 'True', 'column_invisible': None}), ]: view = self.env['ir.ui.view'].create({ 'name': 'foo', 'type': view_type, 'model': 'res.partner', 'arch': arch, }) arch = self.env['res.partner'].with_context(studio=True).get_view(view.id)['arch'] tree = etree.fromstring(arch) node = tree.xpath('//*[@name="name"]')[0] for modifier, value in expected_modifiers.items(): self.assertEqual(node.get(modifier), value) def test_get_view_t_groups(self): """Tests the behavior of blocks with Studio.""" for view_type, arch, expected_modifiers in [ # The user has the group of the `` node, the `` node **must remain**, and be visible. ('form', """
""", {'invisible': None}), # The user doesn't have the group of the `` node, the `` node **must remain**, and be invisible. ('form', """
""", {'invisible': 'True'}), ]: view = self.env['ir.ui.view'].create({ 'name': 'foo', 'type': view_type, 'model': 'res.partner', 'arch': arch, }) arch = self.env['res.partner'].with_context(studio=True).get_view(view.id)['arch'] tree = etree.fromstring(arch) self.assertTrue(tree.xpath('//t')) node = tree.xpath('//t')[0] for modifier, value in expected_modifiers.items(): self.assertEqual(node.get(modifier), value) def test_open_users_form_with_studio(self): """Tests the res.users form view can be loaded with Studio. The res.users form is an edge case, because it uses fake fields in its view, which do not exist in the model. Make sure the Studio overrides regarding the loading of the views, including the postprocessing, are able to handle these non-existing fields. """ arch = self.env['res.users'].with_context(studio=True).get_view(self.env.ref('base.view_users_form').id)['arch'] self.assertTrue(arch) def test_add_many2one_with_custom_rec_name(self): base_view = self.env['ir.ui.view'].create({ 'name': 'TestForm', 'type': 'form', 'model': 'res.partner', 'arch': """
""" }) relation_id = self.env['ir.model'].search([["model", "=", "res.partner.bank"]]).id add_many2one_field_op = { "type": "add", "target": { "tag": "field", "attrs": {"name": "display_name"}, "xpath_info": [ {"tag": "form", "indice": 1}, {"tag": "field", "indice": 1}, ], }, "position": "after", "node": { "tag": "field", "attrs": {}, "field_description": { "type": "many2one", "field_description": "ddd", "special": False, "name": "x_studio_many2one_field_sNT7g", "model_name": "res.partner", "relation_id": relation_id, }, }, } self.edit_view(base_view, operations=[add_many2one_field_op]) self.assertViewArchEqual( base_view.get_combined_arch(), """
""" )