[IMP] base_jsonify: add lang, custom resolvers, alias* support

pull/2418/head
nans 2020-09-07 14:33:38 +02:00 committed by Sébastien BEAU
parent 672e904d38
commit 8380df640e
15 changed files with 624 additions and 101 deletions

View File

@ -29,7 +29,7 @@ This module adds a 'jsonify' method to every model of the ORM.
It works on the current recordset and requires a single argument 'parser'
that specify the field to extract.
Example of parser:
Example of a simple parser:
.. code-block:: python
@ -42,8 +42,8 @@ Example of parser:
('line_id', ['id', ('product_id', ['name']), 'price_unit'])
]
In order to be consitent with the odoo api the jsonify method always
return a list of object even if there is only one element in input
In order to be consistent with the odoo api the jsonify method always
return a list of object even if there is only one element in the recordset.
By default the key into the json is the name of the field extracted
from the model. If you need to specify an alternate name to use as key, you
@ -79,6 +79,77 @@ you can pass a callable or the name of a method on the model:
Also the module provide a method "get_json_parser" on the ir.exports object
that generate a parser from an ir.exports configuration.
Further features are available for advanced uses.
It defines a simple "resolver" model that has a "python_code" field and an eval
function so that arbitrary functions can be configured to transform fields,
or process the resulting dictionary.
It is also to specify a lang to extract the translation of any given field.
To use these features, a full parser follows the following structure:
.. code-block:: python
parser = {
"resolver": ir.exports.resolver(3),
"langs": {
False: [
{'name': 'description'},
{'name': 'number', 'resolver': ir.exports.resolver(5)},
({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'display_name'}])
],
'fr_FR': [
{'name': 'description', 'alias': 'descriptions_fr'},
({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'description', 'alias': 'description_fr'}])
],
}
}
A simple parser is simply translated into a full parser at export.
If the global resolver is given, then the json_dict goes through:
.. code-block:: python
resolver.eval(dict, record)
Which allows to add external data from the context or transform the dictionary
if necessary. Similarly if given for a field the resolver evaluates the result.
It is possible for an alias to end with a '*':
in that case the result it put into a list.
.. code-block:: python
parser = {
"langs": {
False: [
{'name': 'name'},
{'name': 'field_1', 'alias': 'customTags*'},
{'name': 'field_2', 'alias': 'customTags*'},
]
}
}
Would result in the following json structure:
.. code-block:: python
{
'name': 'record_name',
'customTags': ['field_1_value', 'field_2_value'],
}
The intended use-case is to be compatible with APIs that require all translated
parameters to be exported simultaneously, and ask for custom properties to be
put in a sub-dictionary.
Since it is often the case that some of these requirements are optional,
new requirements could be met without needing to add field or change any code.
Note that the export values with the simple parser depends on the record's lang;
this is in contrast with full parsers which are designed to be language agnostic.
**Table of contents**
.. contents::
@ -108,6 +179,7 @@ Contributors
* BEAU Sébastien <sebastien.beau@akretion.com>
* Raphaël Reverdy <raphael.reverdy@akretion.com>
* Laurent Mignon <laurent.mignon@acsone.eu>
* Nans Lefebvre <nans.lefebvre@acsone.eu>
Maintainers
~~~~~~~~~~~

View File

@ -13,6 +13,14 @@
"license": "AGPL-3",
"installable": True,
"depends": ["base"],
"data": ["views/ir_exports_view.xml"],
"demo": ["demo/export_demo.xml", "demo/ir.exports.line.csv"],
"data": [
"security/ir.model.access.csv",
"views/ir_exports_view.xml",
"views/ir_exports_resolver_view.xml",
],
"demo": [
"demo/resolver_demo.xml",
"demo/export_demo.xml",
"demo/ir.exports.line.csv",
],
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="ir_exports_resolver_dict" model="ir.exports.resolver">
<field name="name">ExtraData dictionary (number/text)</field>
<field name="python_code">
is_number = field_type in ('integer', 'float')
ftype = "NUMBER" if is_number else "TEXT"
value = value if is_number else str(value)
result = {"Key": name, "Value": value, "Type": ftype, "IsPublic": True}
</field>
</record>
</odoo>

View File

@ -1,3 +1,4 @@
from . import models
from . import ir_export
from . import ir_exports_line
from . import ir_exports_resolver

View File

@ -4,10 +4,20 @@
from collections import OrderedDict
from odoo import models
from odoo import fields, models
def update_dict(data, fields):
def partition(l, accessor): # -> Dict
result = {}
for item in l:
key = accessor(item)
if key not in result:
result[key] = []
result[key].append(item)
return result
def update_dict(data, fields, options):
"""Contruct a tree of fields.
Example:
@ -23,11 +33,11 @@ def update_dict(data, fields):
if len(fields) == 1:
if field == ".id":
field = "id"
data[field] = True
data[field] = (True, options)
else:
if field not in data:
data[field] = OrderedDict()
update_dict(data[field], fields[1:])
data[field] = (False, OrderedDict())
update_dict(data[field][1], fields[1:], options)
def convert_dict(dict_parser):
@ -37,27 +47,59 @@ def convert_dict(dict_parser):
"""
parser = []
for field, value in dict_parser.items():
if value is True:
parser.append(field)
if value[0] is True: # is a leaf
parser.append(field_dict(field, value[1]))
else:
parser.append((field, convert_dict(value)))
parser.append((field_dict(field), convert_dict(value[1])))
return parser
def field_dict(field, options=None):
result = {"name": field.split(":")[0]}
if len(field.split(":")) > 1:
result["alias"] = field.split(":")[1]
for option in options or {}:
if options[option]:
result[option] = options[option]
return result
class IrExport(models.Model):
_inherit = "ir.exports"
language_agnostic = fields.Boolean(
default=False,
string="Language Agnostic",
help="If set, will set the lang to False when exporting lines without lang,"
" otherwise it uses the lang in the given context to export these fields",
)
global_resolver_id = fields.Many2one(
comodel_name="ir.exports.resolver",
string="Custom global resolver",
domain="[('type', '=', 'global')]",
help="If set, will apply the global resolver to the result",
)
def get_json_parser(self):
"""Creates a parser from ir.exports record and return it.
The final parser can be used to "jsonify" records of ir.export's model.
"""
self.ensure_one()
dict_parser = OrderedDict()
for line in self.export_fields:
names = line.name.split("/")
if line.alias:
names = line.alias.split("/")
update_dict(dict_parser, names)
return convert_dict(dict_parser)
parser = {"language_agnostic": self.language_agnostic}
lang_to_lines = partition(self.export_fields, lambda l: l.lang_id.code)
lang_parsers = {}
for lang in lang_to_lines:
dict_parser = OrderedDict()
for line in lang_to_lines[lang]:
names = line.name.split("/")
if line.alias:
names = line.alias.split("/")
options = {"resolver": line.resolver_id, "function": line.function}
update_dict(dict_parser, names, options)
lang_parsers[lang] = convert_dict(dict_parser)
parser["langs"] = lang_parsers
if self.global_resolver_id:
parser["resolver"] = self.global_resolver_id
return parser

View File

@ -14,6 +14,29 @@ class IrExportsLine(models.Model):
"alias on the a step as field:alias",
)
active = fields.Boolean(string="Active", default=True)
lang_id = fields.Many2one(
comodel_name="res.lang",
string="Language",
help="If set, the language in which the field is exported",
)
resolver_id = fields.Many2one(
comodel_name="ir.exports.resolver",
string="Custom resolver",
help="If set, will apply the resolver on the field value",
)
function = fields.Char(
comodel_name="ir.exports.resolver",
string="Function",
help="A method defined on the model that takes a record and a field_name",
)
@api.constrains("resolver_id", "function")
def _check_function_resolver(self):
for rec in self:
if rec.resolver_id and rec.function:
raise ValidationError(
_("Either set a function or a resolver, not both.")
)
@api.constrains("alias", "name")
def _check_alias(self):

View File

@ -0,0 +1,52 @@
# Copyright 2020 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo.tools.safe_eval import safe_eval
help_message = [
"Compute the result from 'value' by setting the variable 'result'.",
"For fields resolvers:",
":param name: name of the field",
":param value: value of the field",
":param field_type: type of the field",
"For global resolvers:",
":param value: json dict",
":param record: the record",
]
class FieldResolver(models.Model):
"""Arbitrary function to process a field or a dict at export time.
"""
_name = "ir.exports.resolver"
_description = "Resolver"
name = fields.Char()
type = fields.Selection([("field", "Field"), ("global", "Global")])
python_code = fields.Text(
string="Python Code",
default="\n# ".join(["result = value"] + help_message),
help="\n".join(help_message),
)
def eval(self, param, records):
self.ensure_one()
result = []
if self.type == "global":
assert len(param) == len(records)
for value, record in zip(param, records):
values = {"value": value, "record": record}
safe_eval(self.python_code, values, mode="exec", nocopy=True)
result.append(values["result"])
else: # param is a field
for record in records:
values = {
"value": record[param.name],
"name": param.name,
"field_type": param.type,
}
safe_eval(self.python_code, values, mode="exec", nocopy=True)
result.append(values["result"])
return result

View File

@ -17,19 +17,103 @@ class Base(models.AbstractModel):
@api.model
def __parse_field(self, parser_field):
"""Deduct how to handle a field from its parser."""
field_name = parser_field
subparser = None
if isinstance(parser_field, tuple):
field_name, subparser = parser_field
json_key = field_name
if ":" in field_name:
field_name, json_key = field_name.split(":")
return field_name, json_key, subparser
return parser_field if isinstance(parser_field, tuple) else (parser_field, None)
@api.model
def convert_simple_to_full_parser(self, parser):
def _f(f, function=None):
field_split = f.split(":")
field_dict = {"name": field_split[0]}
if len(field_split) > 1:
field_dict["alias"] = field_split[1]
if function:
field_dict["function"] = function
return field_dict
def _convert_parser(parser):
result = []
for line in parser:
if isinstance(line, str):
result.append(_f(line))
else:
f, sub = line
if callable(sub) or isinstance(sub, str):
result.append(_f(f, sub))
else:
result.append((_f(f), _convert_parser(sub)))
return result
return {"language_agnostic": False, "langs": {False: _convert_parser(parser)}}
@api.model
def _jsonify_bad_parser_error(self, field_name):
raise UserError(_("Wrong parser configuration for field: `%s`") % field_name)
def _function_value(self, record, function, field_name):
if function in dir(record):
method = getattr(self, function, None)
return method(field_name)
elif callable(function):
return function(record, field_name)
else:
return self._jsonify_bad_parser_error(field_name)
@api.model
def _jsonify_value(self, record, field, resolver):
# TODO: we should get default by field (eg: char field -> "")
value = record[field.name]
if value is False and field.type != "boolean":
value = None
elif field.type == "date":
value = fields.Date.to_date(value).isoformat()
elif field.type == "datetime":
# Ensures value is a datetime
value = fields.Datetime.to_datetime(value)
# Get the timestamp converted to the client's timezone.
# This call also add the tzinfo into the datetime object
value = fields.Datetime.context_timestamp(record, value)
value = value.isoformat()
return self._resolve(resolver, field, record)[0] if resolver else value
@api.model
def _resolve(self, resolver, *args):
if isinstance(resolver, int):
resolver = self.env["ir.exports.resolver"].browse(resolver)
return resolver.eval(*args)
@api.model
def _jsonify_record(self, parser, rec, root):
for field in parser:
field_dict, subparser = rec.__parse_field(field)
field_name = field_dict["name"]
json_key = field_dict.get("alias", field_name)
field = rec._fields[field_name]
if field_dict.get("function"):
function = field_dict["function"]
value = self._function_value(rec, function, field_name)
elif subparser:
if not (field.relational or field.type == "reference"):
self._jsonify_bad_parser_error(field_name)
value = [
self._jsonify_record(subparser, r, {}) for r in rec[field_name]
]
if field.type in ("many2one", "reference"):
value = value[0] if value else None
else:
value = self._jsonify_value(rec, field, field_dict.get("resolver"))
if json_key.endswith("*"): # sublist field
key = json_key[:-1]
if not root.get(key):
root[key] = []
root[key].append(value)
else:
root[json_key] = value
return root
def jsonify(self, parser, one=False):
"""Convert the record according to the given parser.
Example of parser:
Example of (simple) parser:
parser = [
'name',
'number',
@ -55,68 +139,16 @@ class Base(models.AbstractModel):
"""
if one:
self.ensure_one()
if isinstance(parser, list):
parser = self.convert_simple_to_full_parser(parser)
resolver = parser.get("resolver")
result = []
results = [{} for record in self]
for lang in parser["langs"]:
translate = lang or parser["language_agnostic"]
records = self.with_context(lang=lang) if translate else self
for record, json in zip(records, results):
self._jsonify_record(parser["langs"][lang], record, json)
for rec in self:
res = {}
for field in parser:
field_name, json_key, subparser = self.__parse_field(field)
if subparser:
res[json_key] = rec._jsonify_value_subparser(field_name, subparser)
else:
res[json_key] = rec._jsonify_value(field_name)
result.append(res)
if one:
return result[0] if result else {}
return result
def _jsonify_value(self, field_name):
field_type = self._fields[field_name].type
value = self[field_name]
# TODO: we should get default by field (eg: char field -> "")
if value is False and field_type != "boolean":
value = None
elif field_type == "date":
value = fields.Date.to_date(value).isoformat()
elif field_type == "datetime":
# Ensures value is a datetime
value = fields.Datetime.to_datetime(value)
# Get the timestamp converted to the client's timezone.
# This call also add the tzinfo into the datetime
# object
value = fields.Datetime.context_timestamp(self, value)
value = value.isoformat()
elif field_type in ("many2one", "reference"):
if not value:
value = None
else:
value = value.display_name
elif field_type in ("one2many", "many2many"):
value = [v.display_name for v in value]
return value
def _jsonify_value_subparser(self, field_name, subparser):
value = None
if callable(subparser):
# a simple function
value = subparser(self, field_name)
elif isinstance(subparser, str):
# a method on the record itself
method = getattr(self, subparser, None)
if method:
value = method(field_name)
else:
self._jsonify_bad_parser_error(field_name)
else:
field = self._fields[field_name]
if not (field.relational or field.type == "reference"):
self._jsonify_bad_parser_error(field_name)
rec_value = self[field_name]
value = rec_value.jsonify(subparser) if rec_value else []
if field.type in ("many2one", "reference"):
value = value[0] if value else None
return value
def _jsonify_bad_parser_error(self, field_name):
raise UserError(_("Wrong parser configuration for field: `%s`") % field_name)
results = self._resolve(resolver, results, self) if resolver else results
return results[0] if one else results

View File

@ -1,3 +1,4 @@
* BEAU Sébastien <sebastien.beau@akretion.com>
* Raphaël Reverdy <raphael.reverdy@akretion.com>
* Laurent Mignon <laurent.mignon@acsone.eu>
* Nans Lefebvre <nans.lefebvre@acsone.eu>

View File

@ -2,7 +2,7 @@ This module adds a 'jsonify' method to every model of the ORM.
It works on the current recordset and requires a single argument 'parser'
that specify the field to extract.
Example of parser:
Example of a simple parser:
.. code-block:: python
@ -15,8 +15,8 @@ Example of parser:
('line_id', ['id', ('product_id', ['name']), 'price_unit'])
]
In order to be consitent with the odoo api the jsonify method always
return a list of object even if there is only one element in input
In order to be consistent with the odoo api the jsonify method always
return a list of object even if there is only one element in the recordset.
By default the key into the json is the name of the field extracted
from the model. If you need to specify an alternate name to use as key, you
@ -51,3 +51,74 @@ you can pass a callable or the name of a method on the model:
Also the module provide a method "get_json_parser" on the ir.exports object
that generate a parser from an ir.exports configuration.
Further features are available for advanced uses.
It defines a simple "resolver" model that has a "python_code" field and an eval
function so that arbitrary functions can be configured to transform fields,
or process the resulting dictionary.
It is also to specify a lang to extract the translation of any given field.
To use these features, a full parser follows the following structure:
.. code-block:: python
parser = {
"resolver": ir.exports.resolver(3),
"langs": {
False: [
{'name': 'description'},
{'name': 'number', 'resolver': ir.exports.resolver(5)},
({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'display_name'}])
],
'fr_FR': [
{'name': 'description', 'alias': 'descriptions_fr'},
({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'description', 'alias': 'description_fr'}])
],
}
}
A simple parser is simply translated into a full parser at export.
If the global resolver is given, then the json_dict goes through:
.. code-block:: python
resolver.eval(dict, record)
Which allows to add external data from the context or transform the dictionary
if necessary. Similarly if given for a field the resolver evaluates the result.
It is possible for an alias to end with a '*':
in that case the result it put into a list.
.. code-block:: python
parser = {
"langs": {
False: [
{'name': 'name'},
{'name': 'field_1', 'alias': 'customTags*'},
{'name': 'field_2', 'alias': 'customTags*'},
]
}
}
Would result in the following json structure:
.. code-block:: python
{
'name': 'record_name',
'customTags': ['field_1_value', 'field_2_value'],
}
The intended use-case is to be compatible with APIs that require all translated
parameters to be exported simultaneously, and ask for custom properties to be
put in a sub-dictionary.
Since it is often the case that some of these requirements are optional,
new requirements could be met without needing to add field or change any code.
Note that the export values with the simple parser depends on the record's lang;
this is in contrast with full parsers which are designed to be language agnostic.

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ir_exports_resolver,ir.exports.resolver,model_ir_exports_resolver,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_exports_resolver ir.exports.resolver model_ir_exports_resolver base.group_system 1 1 1 1

View File

@ -371,7 +371,7 @@ ul.auto-toc {
<p>This module adds a jsonify method to every model of the ORM.
It works on the current recordset and requires a single argument parser
that specify the field to extract.</p>
<p>Example of parser:</p>
<p>Example of a simple parser:</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1">'name'</span><span class="p">,</span>
@ -381,8 +381,8 @@ that specify the field to extract.</p>
<span class="p">(</span><span class="s1">'line_id'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'id'</span><span class="p">,</span> <span class="p">(</span><span class="s1">'product_id'</span><span class="p">,</span> <span class="p">[</span><span class="s1">'name'</span><span class="p">]),</span> <span class="s1">'price_unit'</span><span class="p">])</span>
<span class="p">]</span>
</pre>
<p>In order to be consitent with the odoo api the jsonify method always
return a list of object even if there is only one element in input</p>
<p>In order to be consistent with the odoo api the jsonify method always
return a list of object even if there is only one element in the recordset.</p>
<p>By default the key into the json is the name of the field extracted
from the model. If you need to specify an alternate name to use as key, you
can define your mapping as follow into the parser definition:</p>
@ -410,6 +410,62 @@ you can pass a callable or the name of a method on the model:</p>
</pre>
<p>Also the module provide a method “get_json_parser” on the ir.exports object
that generate a parser from an ir.exports configuration.</p>
<p>Further features are available for advanced uses.
It defines a simple “resolver” model that has a “python_code” field and an eval
function so that arbitrary functions can be configured to transform fields,
or process the resulting dictionary.
It is also to specify a lang to extract the translation of any given field.</p>
<p>To use these features, a full parser follows the following structure:</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">&quot;resolver&quot;</span><span class="p">:</span> <span class="n">ir</span><span class="o">.</span><span class="n">exports</span><span class="o">.</span><span class="n">resolver</span><span class="p">(</span><span class="mi">3</span><span class="p">),</span>
<span class="s2">&quot;langs&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="kc">False</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'number'</span><span class="p">,</span> <span class="s1">'resolver'</span><span class="p">:</span> <span class="n">ir</span><span class="o">.</span><span class="n">exports</span><span class="o">.</span><span class="n">resolver</span><span class="p">(</span><span class="mi">5</span><span class="p">)},</span>
<span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'partners'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'display_name'</span><span class="p">}])</span>
<span class="p">],</span>
<span class="s1">'fr_FR'</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'descriptions_fr'</span><span class="p">},</span>
<span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'partners'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'description_fr'</span><span class="p">}])</span>
<span class="p">],</span>
<span class="p">}</span>
<span class="p">}</span>
</pre>
<p>A simple parser is simply translated into a full parser at export.</p>
<p>If the global resolver is given, then the json_dict goes through:</p>
<pre class="code python literal-block">
<span class="n">resolver</span><span class="o">.</span><span class="n">eval</span><span class="p">(</span><span class="nb">dict</span><span class="p">,</span> <span class="n">record</span><span class="p">)</span>
</pre>
<p>Which allows to add external data from the context or transform the dictionary
if necessary. Similarly if given for a field the resolver evaluates the result.</p>
<p>It is possible for an alias to end with a *:
in that case the result it put into a list.</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">&quot;langs&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="kc">False</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'name'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'field_1'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'customTags*'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'field_2'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'customTags*'</span><span class="p">},</span>
<span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>
</pre>
<p>Would result in the following json structure:</p>
<pre class="code python literal-block">
<span class="p">{</span>
<span class="s1">'name'</span><span class="p">:</span> <span class="s1">'record_name'</span><span class="p">,</span>
<span class="s1">'customTags'</span><span class="p">:</span> <span class="p">[</span><span class="s1">'field_1_value'</span><span class="p">,</span> <span class="s1">'field_2_value'</span><span class="p">],</span>
<span class="p">}</span>
</pre>
<p>The intended use-case is to be compatible with APIs that require all translated
parameters to be exported simultaneously, and ask for custom properties to be
put in a sub-dictionary.
Since it is often the case that some of these requirements are optional,
new requirements could be met without needing to add field or change any code.</p>
<p>Note that the export values with the simple parser depends on the records lang;
this is in contrast with full parsers which are designed to be language agnostic.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
@ -444,6 +500,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<li>BEAU Sébastien &lt;<a class="reference external" href="mailto:sebastien.beau&#64;akretion.com">sebastien.beau&#64;akretion.com</a>&gt;</li>
<li>Raphaël Reverdy &lt;<a class="reference external" href="mailto:raphael.reverdy&#64;akretion.com">raphael.reverdy&#64;akretion.com</a>&gt;</li>
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
<li>Nans Lefebvre &lt;<a class="reference external" href="mailto:nans.lefebvre&#64;acsone.eu">nans.lefebvre&#64;acsone.eu</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">

View File

@ -35,6 +35,58 @@ class TestParser(SavepointCase):
"date": fields.Date.from_string("2019-10-31"),
}
)
Langs = cls.env["res.lang"].with_context(active_test=False)
cls.lang = Langs.search([("code", "=", "fr_FR")])
cls.lang.active = True
cls.env["ir.translation"]._load_module_terms(["base"], [cls.lang.code])
category = cls.env["res.partner.category"].create({"name": "name"})
cls.translated_alias = "name_{}".format(cls.lang.code)
cls.env["ir.translation"].create(
{
"type": "model",
"name": "res.partner.category,name",
"module": "base",
"lang": cls.lang.code,
"res_id": category.id,
"value": cls.translated_alias,
"state": "translated",
}
)
cls.global_resolver = cls.env["ir.exports.resolver"].create(
{"python_code": "value['X'] = 'X'; result = value", "type": "global"}
)
cls.resolver = cls.env["ir.exports.resolver"].create(
{"python_code": "result = value + '_pidgin'", "type": "field"}
)
cls.category_export = cls.env["ir.exports"].create(
{
"global_resolver_id": cls.global_resolver.id,
"language_agnostic": True,
"export_fields": [
(0, 0, {"name": "name"}),
(
0,
0,
{
"name": "name",
"alias": "name:{}".format(cls.translated_alias),
"lang_id": cls.lang.id,
},
),
(
0,
0,
{
"name": "name",
"alias": "name:name_resolved",
"resolver_id": cls.resolver.id,
},
),
],
}
)
cls.category = category.with_context({})
cls.category_lang = category.with_context({"lang": cls.lang.code})
def test_getting_parser(self):
expected_parser = [
@ -60,7 +112,8 @@ class TestParser(SavepointCase):
exporter = self.env.ref("base_jsonify.ir_exp_partner")
parser = exporter.get_json_parser()
self.assertListEqual(parser, expected_parser)
expected_full_parser = exporter.convert_simple_to_full_parser(expected_parser)
self.assertEqual(parser, expected_full_parser)
# modify an ir.exports_line to put an alias for a field
self.env.ref("base_jsonify.category_id_name").write(
@ -68,7 +121,8 @@ class TestParser(SavepointCase):
)
expected_parser[4] = ("category_id:category", ["name"])
parser = exporter.get_json_parser()
self.assertEqual(parser, expected_parser)
expected_full_parser = exporter.convert_simple_to_full_parser(expected_parser)
self.assertEqual(parser, expected_full_parser)
def test_json_export(self):
# Enforces TZ to validate the serialization result of a Datetime
@ -162,3 +216,68 @@ class TestParser(SavepointCase):
json_partner = self.partner.jsonify(parser)
self.assertDictEqual(json_partner[0], expected_json)
del self.partner.__class__.jsonify_custom
def test_full_parser(self):
parser = self.category_export.get_json_parser()
json = self.category.jsonify(parser)[0]
json_fr = self.category_lang.jsonify(parser)[0]
self.assertEqual(
json, json_fr
) # starting from different languages should not change anything
self.assertEqual(json[self.translated_alias], self.translated_alias)
self.assertEqual(json["name_resolved"], "name_pidgin") # field resolver
self.assertEqual(json["X"], "X") # added by global resolver
def test_simple_parser_translations(self):
"""The simple parser result should depend on the context language.
"""
parser = ["name"]
json = self.category.jsonify(parser)[0]
json_fr = self.category_lang.jsonify(parser)[0]
self.assertEqual(json["name"], "name")
self.assertEqual(json_fr["name"], self.translated_alias)
def test_simple_star_alias_and_field_resolver(self):
"""The simple parser result should depend on the context language.
"""
code = (
"is_number = field_type in ('integer', 'float');"
"ftype = 'NUMBER' if is_number else 'TEXT';"
"value = value if is_number else str(value);"
"result = {'Key': name, 'Value': value, 'Type': ftype, 'IsPublic': True}"
)
resolver = self.env["ir.exports.resolver"].create({"python_code": code})
lang_parser = [
{"alias": "customTags*", "name": "name", "resolver": resolver.id},
{"alias": "customTags*", "name": "id", "resolver": resolver.id},
]
parser = {"language_agnostic": True, "langs": {False: lang_parser}}
expected_json = {
"customTags": [
{"Value": "name", "Key": "name", "Type": "TEXT", "IsPublic": True},
{
"Value": self.category.id,
"Key": "id",
"Type": "NUMBER",
"IsPublic": True,
},
]
}
json = self.category.jsonify(parser)[0]
self.assertEqual(json, expected_json)
def test_simple_export_with_function(self):
self.category.__class__.jsonify_custom = jsonify_custom
export = self.env["ir.exports"].create(
{
"export_fields": [
(0, 0, {"name": "name", "function": "jsonify_custom"}),
],
}
)
json = self.category.jsonify(export.get_json_parser())[0]
self.assertEqual(json, {"name": "yeah!"})

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.ui.view" id="view_ir_exports_resolver">
<field name="model">ir.exports.resolver</field>
<field name="priority">50</field>
<field name="arch" type="xml">
<form>
<group>
<field name="name" />
<field name="type" />
<field name="python_code" />
</group>
</form>
</field>
</record>
<record id="act_ui_exports_resolver_view" model="ir.actions.act_window">
<field name="name">Custom Export Resolvers</field>
<field name="res_model">ir.exports.resolver</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
id="ui_exports_resolvers"
action="act_ui_exports_resolver_view"
parent="base.next_id_2"
/>
</odoo>

View File

@ -10,6 +10,8 @@
<group colspan="4" col="4" name="se-main">
<field name="name" />
<field name="resource" />
<field name="language_agnostic" />
<field name="global_resolver_id" />
</group>
</group>
<group name="index" string="Index">
@ -17,6 +19,9 @@
<tree editable="bottom">
<field name="name" />
<field name="alias" />
<field name="lang_id" />
<field name="resolver_id" />
<field name="function" />
</tree>
</field>
</group>