[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
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -5,4 +5,7 @@
|
||||
'name': 'test_convert',
|
||||
'description': "Data for xml conversion tests",
|
||||
'version': '0.0.1',
|
||||
'data': [
|
||||
'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
|
||||
|
@@ -1,2 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_convert
|
||||
from . import test_env
|
||||
|
||||
@@ -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')
|
||||
+8
-5
@@ -1,6 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
|
||||
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
|
||||
<rng:define name="env">
|
||||
<rng:optional><rng:attribute name="uid" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="context" /></rng:optional>
|
||||
</rng:define>
|
||||
|
||||
<rng:define name="any">
|
||||
<rng:element>
|
||||
<rng:anyName/>
|
||||
@@ -37,9 +42,7 @@
|
||||
<rng:element name="function">
|
||||
<rng:attribute name="model" />
|
||||
<rng:attribute name="name" />
|
||||
<!-- doesn't look like it has any use? -->
|
||||
<rng:optional><rng:attribute name="id" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="context" /></rng:optional>
|
||||
<rng:ref name="env"/>
|
||||
<rng:optional>
|
||||
<rng:choice>
|
||||
<rng:attribute name="eval" />
|
||||
@@ -163,7 +166,7 @@
|
||||
</rng:optional>
|
||||
</rng:optional>
|
||||
<rng:attribute name="model" />
|
||||
<rng:optional><rng:attribute name="context"/></rng:optional>
|
||||
<rng:ref name="env"/>
|
||||
<rng:zeroOrMore>
|
||||
<rng:ref name="field" />
|
||||
</rng:zeroOrMore>
|
||||
@@ -303,7 +306,7 @@
|
||||
<rng:name>data</rng:name>
|
||||
</rng:choice>
|
||||
<rng:optional><rng:attribute name="noupdate" /></rng:optional>
|
||||
<rng:optional><rng:attribute name="context" /></rng:optional>
|
||||
<rng:ref name="env"/>
|
||||
<rng:zeroOrMore>
|
||||
<rng:choice>
|
||||
<rng:text/>
|
||||
|
||||
@@ -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 *
|
||||
|
||||
+77
-73
@@ -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 <function> 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 <openerp>, <odoo> or <data>."
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user