[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' It works on the current recordset and requires a single argument 'parser'
that specify the field to extract. that specify the field to extract.
Example of parser: Example of a simple parser:
.. code-block:: python .. code-block:: python
@ -42,8 +42,8 @@ Example of parser:
('line_id', ['id', ('product_id', ['name']), 'price_unit']) ('line_id', ['id', ('product_id', ['name']), 'price_unit'])
] ]
In order to be consitent with the odoo api the jsonify method always 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 input 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 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 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 Also the module provide a method "get_json_parser" on the ir.exports object
that generate a parser from an ir.exports configuration. 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** **Table of contents**
.. contents:: .. contents::
@ -108,6 +179,7 @@ Contributors
* BEAU Sébastien <sebastien.beau@akretion.com> * BEAU Sébastien <sebastien.beau@akretion.com>
* Raphaël Reverdy <raphael.reverdy@akretion.com> * Raphaël Reverdy <raphael.reverdy@akretion.com>
* Laurent Mignon <laurent.mignon@acsone.eu> * Laurent Mignon <laurent.mignon@acsone.eu>
* Nans Lefebvre <nans.lefebvre@acsone.eu>
Maintainers Maintainers
~~~~~~~~~~~ ~~~~~~~~~~~

View File

@ -13,6 +13,14 @@
"license": "AGPL-3", "license": "AGPL-3",
"installable": True, "installable": True,
"depends": ["base"], "depends": ["base"],
"data": ["views/ir_exports_view.xml"], "data": [
"demo": ["demo/export_demo.xml", "demo/ir.exports.line.csv"], "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 models
from . import ir_export from . import ir_export
from . import ir_exports_line from . import ir_exports_line
from . import ir_exports_resolver

View File

@ -4,10 +4,20 @@
from collections import OrderedDict 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. """Contruct a tree of fields.
Example: Example:
@ -23,11 +33,11 @@ def update_dict(data, fields):
if len(fields) == 1: if len(fields) == 1:
if field == ".id": if field == ".id":
field = "id" field = "id"
data[field] = True data[field] = (True, options)
else: else:
if field not in data: if field not in data:
data[field] = OrderedDict() data[field] = (False, OrderedDict())
update_dict(data[field], fields[1:]) update_dict(data[field][1], fields[1:], options)
def convert_dict(dict_parser): def convert_dict(dict_parser):
@ -37,27 +47,59 @@ def convert_dict(dict_parser):
""" """
parser = [] parser = []
for field, value in dict_parser.items(): for field, value in dict_parser.items():
if value is True: if value[0] is True: # is a leaf
parser.append(field) parser.append(field_dict(field, value[1]))
else: else:
parser.append((field, convert_dict(value))) parser.append((field_dict(field), convert_dict(value[1])))
return parser 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): class IrExport(models.Model):
_inherit = "ir.exports" _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): def get_json_parser(self):
"""Creates a parser from ir.exports record and return it. """Creates a parser from ir.exports record and return it.
The final parser can be used to "jsonify" records of ir.export's model. The final parser can be used to "jsonify" records of ir.export's model.
""" """
self.ensure_one() self.ensure_one()
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() dict_parser = OrderedDict()
for line in self.export_fields: for line in lang_to_lines[lang]:
names = line.name.split("/") names = line.name.split("/")
if line.alias: if line.alias:
names = line.alias.split("/") names = line.alias.split("/")
update_dict(dict_parser, names) options = {"resolver": line.resolver_id, "function": line.function}
update_dict(dict_parser, names, options)
return convert_dict(dict_parser) 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", "alias on the a step as field:alias",
) )
active = fields.Boolean(string="Active", default=True) 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") @api.constrains("alias", "name")
def _check_alias(self): 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 @api.model
def __parse_field(self, parser_field): def __parse_field(self, parser_field):
"""Deduct how to handle a field from its parser.""" """Deduct how to handle a field from its parser."""
field_name = parser_field return parser_field if isinstance(parser_field, tuple) else (parser_field, None)
subparser = None
if isinstance(parser_field, tuple): @api.model
field_name, subparser = parser_field def convert_simple_to_full_parser(self, parser):
json_key = field_name def _f(f, function=None):
if ":" in field_name: field_split = f.split(":")
field_name, json_key = field_name.split(":") field_dict = {"name": field_split[0]}
return field_name, json_key, subparser 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): def jsonify(self, parser, one=False):
"""Convert the record according to the given parser. """Convert the record according to the given parser.
Example of parser: Example of (simple) parser:
parser = [ parser = [
'name', 'name',
'number', 'number',
@ -55,68 +139,16 @@ class Base(models.AbstractModel):
""" """
if one: if one:
self.ensure_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: results = self._resolve(resolver, results, self) if resolver else results
res = {} return results[0] if one else results
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)

View File

@ -1,3 +1,4 @@
* BEAU Sébastien <sebastien.beau@akretion.com> * BEAU Sébastien <sebastien.beau@akretion.com>
* Raphaël Reverdy <raphael.reverdy@akretion.com> * Raphaël Reverdy <raphael.reverdy@akretion.com>
* Laurent Mignon <laurent.mignon@acsone.eu> * 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' It works on the current recordset and requires a single argument 'parser'
that specify the field to extract. that specify the field to extract.
Example of parser: Example of a simple parser:
.. code-block:: python .. code-block:: python
@ -15,8 +15,8 @@ Example of parser:
('line_id', ['id', ('product_id', ['name']), 'price_unit']) ('line_id', ['id', ('product_id', ['name']), 'price_unit'])
] ]
In order to be consitent with the odoo api the jsonify method always 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 input 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 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 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 Also the module provide a method "get_json_parser" on the ir.exports object
that generate a parser from an ir.exports configuration. 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. <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 It works on the current recordset and requires a single argument parser
that specify the field to extract.</p> 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"> <pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">[</span> <span class="n">parser</span> <span class="o">=</span> <span class="p">[</span>
<span class="s1">'name'</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><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> <span class="p">]</span>
</pre> </pre>
<p>In order to be consitent with the odoo api the jsonify method always <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 input</p> 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 <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 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> 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> </pre>
<p>Also the module provide a method “get_json_parser” on the ir.exports object <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> 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> <p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents"> <div class="contents local topic" id="contents">
<ul class="simple"> <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>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>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>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> </ul>
</div> </div>
<div class="section" id="maintainers"> <div class="section" id="maintainers">

View File

@ -35,6 +35,58 @@ class TestParser(SavepointCase):
"date": fields.Date.from_string("2019-10-31"), "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): def test_getting_parser(self):
expected_parser = [ expected_parser = [
@ -60,7 +112,8 @@ class TestParser(SavepointCase):
exporter = self.env.ref("base_jsonify.ir_exp_partner") exporter = self.env.ref("base_jsonify.ir_exp_partner")
parser = exporter.get_json_parser() 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 # modify an ir.exports_line to put an alias for a field
self.env.ref("base_jsonify.category_id_name").write( self.env.ref("base_jsonify.category_id_name").write(
@ -68,7 +121,8 @@ class TestParser(SavepointCase):
) )
expected_parser[4] = ("category_id:category", ["name"]) expected_parser[4] = ("category_id:category", ["name"])
parser = exporter.get_json_parser() 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): def test_json_export(self):
# Enforces TZ to validate the serialization result of a Datetime # Enforces TZ to validate the serialization result of a Datetime
@ -162,3 +216,68 @@ class TestParser(SavepointCase):
json_partner = self.partner.jsonify(parser) json_partner = self.partner.jsonify(parser)
self.assertDictEqual(json_partner[0], expected_json) self.assertDictEqual(json_partner[0], expected_json)
del self.partner.__class__.jsonify_custom 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"> <group colspan="4" col="4" name="se-main">
<field name="name" /> <field name="name" />
<field name="resource" /> <field name="resource" />
<field name="language_agnostic" />
<field name="global_resolver_id" />
</group> </group>
</group> </group>
<group name="index" string="Index"> <group name="index" string="Index">
@ -17,6 +19,9 @@
<tree editable="bottom"> <tree editable="bottom">
<field name="name" /> <field name="name" />
<field name="alias" /> <field name="alias" />
<field name="lang_id" />
<field name="resolver_id" />
<field name="function" />
</tree> </tree>
</field> </field>
</group> </group>