[IMP] base_jsonify: add lang, custom resolvers, alias* support
parent
672e904d38
commit
8380df640e
|
@ -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
|
||||
~~~~~~~~~~~
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -1,3 +1,4 @@
|
|||
from . import models
|
||||
from . import ir_export
|
||||
from . import ir_exports_line
|
||||
from . import ir_exports_resolver
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
|
@ -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">"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">3</span><span class="p">),</span>
|
||||
<span class="s2">"langs"</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">"langs"</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 record’s 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 <<a class="reference external" href="mailto:sebastien.beau@akretion.com">sebastien.beau@akretion.com</a>></li>
|
||||
<li>Raphaël Reverdy <<a class="reference external" href="mailto:raphael.reverdy@akretion.com">raphael.reverdy@akretion.com</a>></li>
|
||||
<li>Laurent Mignon <<a class="reference external" href="mailto:laurent.mignon@acsone.eu">laurent.mignon@acsone.eu</a>></li>
|
||||
<li>Nans Lefebvre <<a class="reference external" href="mailto:nans.lefebvre@acsone.eu">nans.lefebvre@acsone.eu</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
|
|
|
@ -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!"})
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue