From bf0ed03d0f133633da17fb8917df369799286f81 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 1 Oct 2018 14:45:50 +0000 Subject: [PATCH] [FIX] core: support for uid/context in XML data files * add tests for uid/context on record & function & root nodes (odoo, data, openerp) "inherited" by record & function * add rng validation to the test cases to ensure technical capabilities are matched by validation capabilities (which was not the case as we could validate nonsense and import stuff which didn't pass validation or something) * extact uid/context into its own rng pattern - add uid support to record validation - add uid support to root nodes validation - fix uid support on function validation: was typoed to @id, which is not used anywhere in function tags loading & makes no sense as far as I can tell * replace get_uid/get_context by get_env & reimplement the whole mess: build a stack of environments from the root nodes (each root node = one env in the stack, though the exact same env can be present multiple times if none of the root nodes has a @uid or @context) and leaves can build upon this "ready made" environment for their own uses or use it as-is. As a result, all uid/context should now properly transfer from the root nodes to the leaves needing them * fix support for context on functions: was not actually implemented as the context was not propagated into the old-style API call * add uid support to record & context is now inherited from env --- odoo/addons/test_convert/__init__.py | 10 ++ odoo/addons/test_convert/__manifest__.py | 3 + odoo/addons/test_convert/ir.model.access.csv | 2 + odoo/addons/test_convert/tests/__init__.py | 1 + odoo/addons/test_convert/tests/test_env.py | 166 +++++++++++++++++++ odoo/import_xml.rng | 13 +- odoo/tools/__init__.py | 2 +- odoo/tools/convert.py | 150 +++++++++-------- 8 files changed, 268 insertions(+), 79 deletions(-) create mode 100644 odoo/addons/test_convert/ir.model.access.csv create mode 100644 odoo/addons/test_convert/tests/test_env.py diff --git a/odoo/addons/test_convert/__init__.py b/odoo/addons/test_convert/__init__.py index e69de29bb2d..a301ab24b52 100644 --- a/odoo/addons/test_convert/__init__.py +++ b/odoo/addons/test_convert/__init__.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class Usered(models.Model): + _name = 'test_convert.usered' + _description = "z test model ignore" + + name = fields.Char() + user_id = fields.Many2one('res.users', default=lambda self: self.env.user) + tz = fields.Char(default=lambda self: self.env.context.get('tz') or self.env.user.tz) diff --git a/odoo/addons/test_convert/__manifest__.py b/odoo/addons/test_convert/__manifest__.py index af6cd1af6e4..1411f093f90 100644 --- a/odoo/addons/test_convert/__manifest__.py +++ b/odoo/addons/test_convert/__manifest__.py @@ -5,4 +5,7 @@ 'name': 'test_convert', 'description': "Data for xml conversion tests", 'version': '0.0.1', + 'data': [ + 'ir.model.access.csv', + ] } diff --git a/odoo/addons/test_convert/ir.model.access.csv b/odoo/addons/test_convert/ir.model.access.csv new file mode 100644 index 00000000000..0282a269c41 --- /dev/null +++ b/odoo/addons/test_convert/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_test_convert_usered,access_test_convert_usered,model_test_convert_usered,base.group_user,1,1,1,1 diff --git a/odoo/addons/test_convert/tests/__init__.py b/odoo/addons/test_convert/tests/__init__.py index 2b608870b30..14d4ce40ead 100644 --- a/odoo/addons/test_convert/tests/__init__.py +++ b/odoo/addons/test_convert/tests/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- from . import test_convert +from . import test_env diff --git a/odoo/addons/test_convert/tests/test_env.py b/odoo/addons/test_convert/tests/test_env.py new file mode 100644 index 00000000000..fc5d9a094d3 --- /dev/null +++ b/odoo/addons/test_convert/tests/test_env.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- +""" +Tests the ability to update environmental information on various nodes (e.g. +change user, add context keys, ...) +""" +import os.path + +from lxml import etree +from lxml.builder import E + +from odoo.tests import common +from odoo.tools import config +from odoo.tools.convert import xml_import + +odoo = E.odoo +data = E.data +record = E.record +field = E.field +function = E.function + +class TestEnv(common.TransactionCase): + def setUp(self): + super().setUp() + self._importer = xml_import(self.env.cr, 'test_convert', None, 'init') + + def importer(self, doc): + etree.RelaxNG( + etree.parse( + os.path.join(config['root_path'], 'import_xml.rng') + ) + ).assert_(doc) + self._importer.parse(doc) + + def test_uid_data_record(self): + self.importer( + odoo( + record( + field("a", name="name"), + model="test_convert.usered", + id="test_convert.testing" + ), + uid="base.user_demo" + ) + ) + + r = self.env.ref('test_convert.testing') + self.assertEqual(r.name, 'a') + self.assertEqual(r.create_uid, self.env.ref('base.user_demo')) + self.assertEqual(r.user_id, self.env.ref('base.user_demo')) + + def test_uid_data_function(self): + self.importer( + odoo( + function( + model="test_convert.usered", + name="create", + eval="[[{'name': 'b'}]]", + ), + uid="base.user_demo" + ) + ) + + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'b') + self.assertEqual(r.create_uid, self.env.ref('base.user_demo')) + self.assertEqual(r.user_id, self.env.ref('base.user_demo')) + + def test_uid_record(self): + self.importer( + odoo( + record( + field('c', name="name"), + model="test_convert.usered", + id="test_convert.testing", + uid="base.user_demo" + ), + uid="base.user_root" + ) + ) + + r = self.env.ref('test_convert.testing') + self.assertEqual(r.name, 'c') + self.assertEqual(r.create_uid, self.env.ref('base.user_demo')) + self.assertEqual(r.user_id, self.env.ref('base.user_demo')) + + + def test_uid_function(self): + self.importer( + odoo( + function( + model="test_convert.usered", + name="create", + uid="base.user_demo", + eval="[[{'name': 'd'}]]" + ), + uid="base.user_root" + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'd') + self.assertEqual(r.create_uid, self.env.ref('base.user_demo')) + self.assertEqual(r.user_id, self.env.ref('base.user_demo')) + + def test_context_data_function(self): + self.env.user.tz = 'UTC' + self.importer( + odoo( + function( + model="test_convert.usered", + name="create", + eval="[[{'name': 'e'}]]", + ), + context="{'tz': 'Asia/Kabul'}", + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'e') + self.assertEqual(r.tz, 'Asia/Kabul') + + def test_context_function(self): + self.env.user.tz = 'UTC' + self.importer( + odoo( + function( + model="test_convert.usered", + name="create", + context="{'tz': 'Pacific/Apia'}", + eval="[[{'name': 'e'}]]", + ), + context="{'tz': 'Asia/Kabul'}", + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'e') + self.assertEqual(r.tz, 'Pacific/Apia') + + def test_context_data_record(self): + self.env.user.tz = 'UTC' + self.importer( + odoo( + record( + field("f", name="name"), + model="test_convert.usered", + ), + context="{'tz': 'America/Knox_IN'}" + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'f') + self.assertEqual(r.tz, 'America/Knox_IN') + + def test_context_record(self): + self.env.user.tz = 'UTC' + self.importer( + odoo( + record( + field("f", name="name"), + model="test_convert.usered", + context="{'tz': 'America/Adak'}", + ), + context="{'tz': 'America/Knox_IN'}" + ) + ) + r = self.env['test_convert.usered'].search([]) + self.assertEqual(r.name, 'f') + self.assertEqual(r.tz, 'America/Adak') diff --git a/odoo/import_xml.rng b/odoo/import_xml.rng index 52ff0f1b420..ccc75af1665 100644 --- a/odoo/import_xml.rng +++ b/odoo/import_xml.rng @@ -1,6 +1,11 @@ + + + + + @@ -37,9 +42,7 @@ - - - + @@ -163,7 +166,7 @@ - + @@ -303,7 +306,7 @@ data - + diff --git a/odoo/tools/__init__.py b/odoo/tools/__init__.py index 6e89bdd9d43..2381f8c3472 100644 --- a/odoo/tools/__init__.py +++ b/odoo/tools/__init__.py @@ -7,7 +7,6 @@ from . import appdirs from . import pdf from .config import config from .misc import * -from .convert import * from .translate import * from .graph import graph from .image import * @@ -18,3 +17,4 @@ from .func import * from .debugger import * from .xml_utils import * from .date_utils import * +from .convert import * diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py index 4363fa25de6..0399394f887 100644 --- a/odoo/tools/convert.py +++ b/odoo/tools/convert.py @@ -19,7 +19,7 @@ from . import assertion_report, pycompat from .config import config from .misc import file_open, unquote, ustr, SKIPPED_ELEMENT_TYPES from .translate import _ -from odoo import SUPERUSER_ID +from odoo import SUPERUSER_ID, api _logger = logging.getLogger(__name__) @@ -177,20 +177,32 @@ def _eval_xml(self, node, env): return tuple(res) return res elif node.tag == "function": - args = [] a_eval = node.get('eval') - # FIXME: should probably be exclusive if a_eval: self.idref['ref'] = self.id_get - args = safe_eval(a_eval, self.idref) - for n in node: - return_val = _eval_xml(self, n, env) - if return_val is not None: - args.append(return_val) + # ensure the args are a list (sometimes folks eval a tuple which + # is inconvenient when trying to concatenate w/ a list) + args = list(safe_eval(a_eval, self.idref)) + else: + args = [ + r for r in (_eval_xml(self, n, env) for n in node) + if r is not None + ] + model = env[node.get('model')] - method = node.get('name') - # this one still depends on the old API - return odoo.api.call_kw(model, method, args, {}) + method_name = node.get('name') + method = getattr(model, method_name) + + # this mess is necessary to merge @context and a possible positional + # context parameter, as works on the old API so ids and + # context are just args + ids, args = [], list(args) + if getattr(method, '_api', None) not in ('model', 'model_create'): + ids, args = args[:1], args[1:] + context, args, kwargs = api.split_context(method, args, {}) + kwargs['context'] = {**env.context, **(context or {})} + + return odoo.api.call_kw(model, method_name, ids + args, kwargs) elif node.tag == "test": return node.text @@ -213,33 +225,21 @@ class xml_import(object): def isnoupdate(self, data_node=None): return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False)) - def get_context(self, data_node, node, eval_dict): - data_node_context = (len(data_node) and data_node.get('context','')) - node_context = node.get("context") - context = {} - for ctx in (data_node_context, node_context): - if ctx: - try: - ctx_res = safe_eval(ctx, eval_dict) - if isinstance(context, dict): - context.update(ctx_res) - else: - context = ctx_res - except (ValueError, NameError): - # Some contexts contain references that are only valid at runtime at - # client-side, so in that case we keep the original context string - # as it is. We also log it, just in case. - context = ctx - _logger.debug('Context value (%s) for element with id "%s" or its data node does not parse '\ - 'at server-side, keeping original string, in case it\'s meant for client side only', - ctx, node.get('id','n/a'), exc_info=True) - return context - - def get_uid(self, data_node, node): - node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid','')) - if node_uid: - return self.id_get(node_uid) - return self.env.user.id + def get_env(self, node, eval_context=None): + uid = node.get('uid') + context = node.get('context') + if uid or context: + return self.env( + user=uid and self.id_get(uid), + context=context and { + **self.env.context, + **safe_eval(context, { + 'ref': self.id_get, + **(eval_context or {}) + }) + } + ) + return self.env def make_xml_id(self, xml_id): if not xml_id or '.' in xml_id: @@ -335,9 +335,7 @@ form: module.record_id""" % (xml_id,) def _tag_function(self, rec, data_node): if self.isnoupdate(data_node) and self.mode != 'init': return - context = self.get_context(data_node, rec, {'ref': self.id_get}) - uid = self.get_uid(data_node, rec) - env = self.env(user=uid, context=context) + env = self.get_env(rec) _eval_xml(self, rec, env) def _tag_act_window(self, rec, data_node): @@ -388,9 +386,8 @@ form: module.record_id""" % (xml_id,) 'active_id': active_id, 'active_ids': active_ids, 'active_model': active_model, - 'ref': self.id_get, } - context = self.get_context(data_node, rec, eval_context) + context = self.get_env(rec, eval_context).context try: domain = safe_eval(domain, eval_context) @@ -516,14 +513,11 @@ form: module.record_id""" % (xml_id,) rec_string = rec.get("string") or 'unknown' records = None - eval_dict = {'ref': self.id_get} - context = self.get_context(data_node, rec, eval_dict) - uid = self.get_uid(data_node, rec) - env = self.env(user=uid, context=context) + env = self.get_env(rec) if rec_id: records = env[rec_model].browse(self.id_get(rec_id)) elif rec_src: - q = safe_eval(rec_src, eval_dict) + q = safe_eval(rec_src, {'ref': self.id_get}) records = env[rec_model].search(q) if rec_src_count: count = int(rec_src_count) @@ -547,7 +541,6 @@ form: module.record_id""" % (xml_id,) globals_dict['_ref'] = ref for test in rec.findall('./test'): f_expr = test.get("expr",'') - env = self.env(user=uid, context=context) expected_value = _eval_xml(self, test, env) or True expression_value = safe_eval(f_expr, globals_dict) if expression_value != expected_value: # assertion failed @@ -564,15 +557,16 @@ form: module.record_id""" % (xml_id,) def _tag_record(self, rec, data_node): rec_model = rec.get("model") - model = self.env[rec_model] + env = self.get_env(rec) rec_id = rec.get("id", '') - rec_context = rec.get("context", {}) - if rec_context: - rec_context = safe_eval(rec_context) + + model = env[rec_model] if self.xml_filename and rec_id: - rec_context['install_module'] = self.module - rec_context['install_filename'] = self.xml_filename + model = model.with_context( + install_module=self.module, + install_filename=self.xml_filename + ) self._test_xml_id(rec_id) xid = self.make_xml_id(rec_id) @@ -585,7 +579,7 @@ form: module.record_id""" % (xml_id,) if not rec_id: return None - record = self.env['ir.model.data']._load_xmlid(xid) + record = env['ir.model.data']._load_xmlid(xid) if record: # if the resource already exists, don't update it but store # its database id (can be useful) @@ -609,16 +603,13 @@ form: module.record_id""" % (xml_id,) f_val = False if f_search: - context = self.get_context(data_node, rec, {'ref': self.id_get}) - uid = self.get_uid(data_node, rec) - env = self.env(user=uid, context=context) idref2 = _get_idref(self, env, f_model, self.idref) q = safe_eval(f_search, idref2) assert f_model, 'Define an attribute model="..." in your .XML file !' # browse the objects searched - s = self.env[f_model].search(q) + s = env[f_model].search(q) # column definitions of the "local" object - _fields = self.env[rec_model]._fields + _fields = env[rec_model]._fields # if the current field is many2many if (f_name in _fields) and _fields[f_name].type == 'many2many': f_val = [(6, 0, [x[f_use] for x in s])] @@ -633,7 +624,7 @@ form: module.record_id""" % (xml_id,) else: f_val = self.id_get(f_ref) else: - f_val = _eval_xml(self, field, self.env) + f_val = _eval_xml(self, field, env) if f_name in model._fields: if model._fields[f_name].type == 'integer': f_val = int(f_val) @@ -644,11 +635,11 @@ form: module.record_id""" % (xml_id,) res[f_name] = f_val data = dict(xml_id=xid, values=res, noupdate=self.isnoupdate(data_node)) - record = model.with_context(rec_context)._load_records([data], self.mode == 'update') + record = model._load_records([data], self.mode == 'update') if rec_id: self.idref[rec_id] = record.id if config.get('import_partial'): - self.env.cr.commit() + env.cr.commit() return rec_model, record.id def _tag_template(self, el, data_node): @@ -735,20 +726,21 @@ form: module.record_id""" % (xml_id,) f = self._tags.get(rec.tag) if f is None: continue + + self.envs.append(self.get_env(el)) try: f(rec, el) - except Exception as e: - exc_info = sys.exc_info() - pycompat.reraise( - ParseError, - ParseError(ustr(e), etree.tostring(rec, encoding='unicode').rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), - exc_info[2] - ) + finally: + self.envs.pop() + + @property + def env(self): + return self.envs[-1] def __init__(self, cr, module, idref, mode, report=None, noupdate=False, xml_filename=None): self.mode = mode self.module = module - self.env = odoo.api.Environment(cr, SUPERUSER_ID, {}) + self.envs = [odoo.api.Environment(cr, SUPERUSER_ID, {})] self.idref = {} if idref is None else idref if report is None: report = assertion_report.assertion_report() @@ -767,9 +759,21 @@ form: module.record_id""" % (xml_id,) **dict.fromkeys(self.DATA_ROOTS, self._tag_root) } + def parse(self, de): assert de.tag in self.DATA_ROOTS, "Root xml tag must be , or ." - self._tag_root(de, de) + try: + self._tag_root(de, de) + except Exception as e: + exc_info = sys.exc_info() + pycompat.reraise( + ParseError, + ParseError( + ustr(e), + etree.tostring(de, encoding='unicode').rstrip(), + de.getroottree().docinfo.URL, de.sourceline), + exc_info[2] + ) DATA_ROOTS = ['odoo', 'data', 'openerp'] def convert_file(cr, module, filename, idref, mode='update', noupdate=False, kind=None, report=None, pathname=None):