# Part of Odoo. See LICENSE file for full copyright and licensing details. import ast import collections import functools import inspect import json import logging import pprint import re import uuid import warnings from itertools import chain from lxml import etree from lxml.etree import LxmlError from lxml.builder import E import odoo from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError, AccessError, UserError from odoo.http import request from odoo.modules.module import get_resource_from_path from odoo.tools import config, ConstantMapping, get_diff, pycompat, apply_inheritance_specs, locate_node, str2bool from odoo.tools import safe_eval, lazy, lazy_property, frozendict from odoo.tools.convert import _fix_multiple_roots from odoo.tools.misc import file_path from odoo.tools.translate import xml_translate, TRANSLATED_ATTRS from odoo.tools.view_validation import valid_view, get_domain_value_names, get_expression_field_names, get_dict_asts from odoo.models import check_method_name from odoo.osv.expression import expression _logger = logging.getLogger(__name__) MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath', 'data-oe-source-id'] VIEW_MODIFIERS = ('column_invisible', 'invisible', 'readonly', 'required') # Some views have a js compiler that generates an owl template from the arch. In that template, # `__comp__` is a reserved keyword giving access to the component instance (e.g. the form renderer # or the kanban record). However, we don't want to see implementation details leaking in archs, so # we use the following regex to detect the use of `__comp__` in dynamic attributes, to forbid it. COMP_REGEX = r'(^|[^\w])\s*__comp__\s*([^\w]|$)' ref_re = re.compile(r""" # first match 'form_view_ref' key, backrefs are used to handle single or # double quoting of the value (['"])(?P\w+_view_ref)\1 # colon separator (with optional spaces around) \s*:\s* # open quote for value (['"]) (?P # we'll just match stuff which is normally part of an xid: # word and "." characters [.\w]+ ) # close with same quote as opening \3 """, re.VERBOSE) def att_names(name): yield name yield f"t-att-{name}" yield f"t-attf-{name}" @lazy def keep_query(): mod = odoo.addons.base.models.ir_qweb warnings.warn(f"keep_query has been moved to {mod}", DeprecationWarning) return mod.keep_query class ViewCustom(models.Model): _name = 'ir.ui.view.custom' _description = 'Custom View' _order = 'create_date desc' # search(limit=1) should return the last customization _rec_name = 'user_id' _allow_sudo_commands = False ref_id = fields.Many2one('ir.ui.view', string='Original View', index=True, required=True, ondelete='cascade') user_id = fields.Many2one('res.users', string='User', index=True, required=True, ondelete='cascade') arch = fields.Text(string='View Architecture', required=True) def _auto_init(self): res = super(ViewCustom, self)._auto_init() tools.create_index(self._cr, 'ir_ui_view_custom_user_id_ref_id', self._table, ['user_id', 'ref_id']) return res def _hasclass(context, *cls): """ Checks if the context node has all the classes passed as arguments """ node_classes = set(context.context_node.attrib.get('class', '').split()) return node_classes.issuperset(cls) def get_view_arch_from_file(filepath, xmlid): module, view_id = xmlid.split('.') xpath = f"//*[@id='{xmlid}' or @id='{view_id}']" # when view is created from model with inheritS of ir_ui_view, the # xmlid has been suffixed by '_ir_ui_view'. We need to also search # for views without this prefix. if view_id.endswith('_ir_ui_view'): # len('_ir_ui_view') == 11 xpath = xpath[:-1] + f" or @id='{xmlid[:-11]}' or @id='{view_id[:-11]}']" document = etree.parse(filepath) for node in document.xpath(xpath): if node.tag == 'record': field_arch = node.find('field[@name="arch"]') if field_arch is not None: _fix_multiple_roots(field_arch) inner = ''.join( etree.tostring(child, encoding='unicode') for child in field_arch.iterchildren() ) return field_arch.text + inner field_view = node.find('field[@name="view_id"]') if field_view is not None: ref_module, _, ref_view_id = field_view.attrib.get('ref').rpartition('.') ref_xmlid = f'{ref_module or module}.{ref_view_id}' return get_view_arch_from_file(filepath, ref_xmlid) return None elif node.tag == 'template': # The following dom operations has been copied from convert.py's _tag_template() if not node.get('inherit_id'): node.set('t-name', xmlid) node.tag = 't' else: node.tag = 'data' node.attrib.pop('id', None) return etree.tostring(node, encoding='unicode') _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filepath, xmlid) return None xpath_utils = etree.FunctionNamespace(None) xpath_utils['hasclass'] = _hasclass TRANSLATED_ATTRS_RE = re.compile(r"@(%s)\b" % "|".join(TRANSLATED_ATTRS)) WRONGCLASS = re.compile(r"(@class\s*=|=\s*@class|contains\(@class)") class View(models.Model): _name = 'ir.ui.view' _description = 'View' _order = "priority,name,id" _allow_sudo_commands = False name = fields.Char(string='View Name', required=True) model = fields.Char(index=True) key = fields.Char(index='btree_not_null') priority = fields.Integer(string='Sequence', default=16, required=True) type = fields.Selection([('tree', 'Tree'), ('form', 'Form'), ('graph', 'Graph'), ('pivot', 'Pivot'), ('calendar', 'Calendar'), ('gantt', 'Gantt'), ('kanban', 'Kanban'), ('search', 'Search'), ('qweb', 'QWeb')], string='View Type') arch = fields.Text(compute='_compute_arch', inverse='_inverse_arch', string='View Architecture', help="""This field should be used when accessing view arch. It will use translation. Note that it will read `arch_db` or `arch_fs` if in dev-xml mode.""") arch_base = fields.Text(compute='_compute_arch_base', inverse='_inverse_arch_base', string='Base View Architecture', help="This field is the same as `arch` field without translations") arch_db = fields.Text(string='Arch Blob', translate=xml_translate, help="This field stores the view arch.") arch_fs = fields.Char(string='Arch Filename', help="""File from where the view originates. Useful to (hard) reset broken views or to read arch from file in dev-xml mode.""") arch_updated = fields.Boolean(string='Modified Architecture') arch_prev = fields.Text(string='Previous View Architecture', help="""This field will save the current `arch_db` before writing on it. Useful to (soft) reset a broken view.""") inherit_id = fields.Many2one('ir.ui.view', string='Inherited View', ondelete='restrict', index=True) inherit_children_ids = fields.One2many('ir.ui.view', 'inherit_id', string='Views which inherit from this one') model_data_id = fields.Many2one('ir.model.data', string="Model Data", compute='_compute_model_data_id', search='_search_model_data_id') xml_id = fields.Char(string="External ID", compute='_compute_xml_id', help="ID of the view defined in xml file") groups_id = fields.Many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id', string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only.") mode = fields.Selection([('primary', "Base view"), ('extension', "Extension View")], string="View inheritance mode", default='primary', required=True, help="""Only applies if this view inherits from an other one (inherit_id is not False/Null). * if extension (default), if this view is requested the closest primary view is looked up (via inherit_id), then all views inheriting from it with this view's model are applied * if primary, the closest primary view is fully resolved (even if it uses a different model than this one), then this view's inheritance specs () are applied, and the result is used as if it were this view's actual arch. """) # The "active" field is not updated during updates if