[REF] base_jsonify: review changes

pull/2418/head
nans 2021-01-19 17:27:20 +01:00 committed by Sébastien BEAU
parent 74a4c2c713
commit d30ea05fcf
13 changed files with 105 additions and 83 deletions

View File

@ -80,7 +80,7 @@ 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. Further features are available for advanced uses.
It defines a simple "resolver" model that has a "python_code" field and an eval It defines a simple "resolver" model that has a "python_code" field and a resolve
function so that arbitrary functions can be configured to transform fields, function so that arbitrary functions can be configured to transform fields,
or process the resulting dictionary. or process the resulting dictionary.
It is also to specify a lang to extract the translation of any given field. It is also to specify a lang to extract the translation of any given field.
@ -152,12 +152,12 @@ If the global resolver is given, then the json_dict goes through:
.. code-block:: python .. code-block:: python
resolver.eval(dict, record) resolver.resolve(dict, record)
Which allows to add external data from the context or transform the dictionary 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. if necessary. Similarly if given for a field the resolver evaluates the result.
It is possible for a target to end with a '*': It is possible for a target to have a marshaller by ending the target with '=list':
in that case the result is put into a list. in that case the result is put into a list.
.. code-block:: python .. code-block:: python
@ -165,8 +165,8 @@ in that case the result is put into a list.
parser = { parser = {
fields: [ fields: [
{'name': 'name'}, {'name': 'name'},
{'name': 'field_1', 'target': 'customTags*'}, {'name': 'field_1', 'target': 'customTags=list'},
{'name': 'field_2', 'target': 'customTags*'}, {'name': 'field_2', 'target': 'customTags=list'},
] ]
} }

View File

@ -1,6 +1,8 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openupgradelib import openupgrade
def migrate(cr, version):
query = """ALTER TABLE "ir_exports_line" RENAME COLUMN "alias" TO "target";""" @openupgrade.migrate()
cr.execute(query) def migrate(env, version):
openupgrade.rename_columns(env.cr, {"ir_exports_line": [("alias", "target")]})

View File

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

View File

@ -8,9 +8,9 @@ from odoo import fields, models
def partition(l, accessor): def partition(l, accessor):
"""Partition an iterable (e.g. a recordset) according to an accessor (e.g. a lambda) """Partition a recordset according to an accessor (e.g. a lambda).
Return a dictionary whose keys are the values obtained from accessor, and values Returns a dictionary whose keys are the values obtained from accessor,
are the items that have this value. and values are the items that have this value.
Example: partition([{"name": "ax"}, {"name": "by"}], lambda x: "x" in x["name"]) Example: partition([{"name": "ax"}, {"name": "by"}], lambda x: "x" in x["name"])
=> {True: [{"name": "ax"}], False: [{"name": "by"}]} => {True: [{"name": "ax"}], False: [{"name": "by"}]}
""" """
@ -61,6 +61,7 @@ def convert_dict(dict_parser):
def field_dict(field, options=None): def field_dict(field, options=None):
"""Create a parser dict for the field field."""
result = {"name": field.split(":")[0]} result = {"name": field.split(":")[0]}
if len(field.split(":")) > 1: if len(field.split(":")) > 1:
result["target"] = field.split(":")[1] result["target"] = field.split(":")[1]
@ -93,7 +94,7 @@ class IrExport(models.Model):
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} parser = {}
lang_to_lines = partition(self.export_fields, lambda l: l.lang_id.code) lang_to_lines = partition(self.export_fields, lambda l: l.lang_id.code)
lang_parsers = {} lang_parsers = {}
for lang in lang_to_lines: for lang in lang_to_lines:
@ -102,7 +103,8 @@ class IrExport(models.Model):
names = line.name.split("/") names = line.name.split("/")
if line.target: if line.target:
names = line.target.split("/") names = line.target.split("/")
options = {"resolver": line.resolver_id, "function": line.function} function = line.instance_method_name
options = {"resolver": line.resolver_id, "function": function}
update_dict(dict_parser, names, options) update_dict(dict_parser, names, options)
lang_parsers[lang] = convert_dict(dict_parser) lang_parsers[lang] = convert_dict(dict_parser)
if list(lang_parsers.keys()) == [False]: if list(lang_parsers.keys()) == [False]:
@ -111,4 +113,6 @@ class IrExport(models.Model):
parser["langs"] = lang_parsers parser["langs"] = lang_parsers
if self.global_resolver_id: if self.global_resolver_id:
parser["resolver"] = self.global_resolver_id parser["resolver"] = self.global_resolver_id
if self.language_agnostic:
parser["language_agnostic"] = self.language_agnostic
return parser return parser

View File

@ -24,19 +24,18 @@ class IrExportsLine(models.Model):
string="Custom resolver", string="Custom resolver",
help="If set, will apply the resolver on the field value", help="If set, will apply the resolver on the field value",
) )
function = fields.Char( instance_method_name = fields.Char(
comodel_name="ir.exports.resolver", comodel_name="ir.exports.resolver",
string="Function", string="Function",
help="A method defined on the model that takes a record and a field_name", help="A method defined on the model that takes a record and a field_name",
) )
@api.constrains("resolver_id", "function") @api.constrains("resolver_id", "instance_method_name")
def _check_function_resolver(self): def _check_function_resolver(self):
for rec in self: for rec in self:
if rec.resolver_id and rec.function: if rec.resolver_id and rec.instance_method_name:
raise ValidationError( msg = _("Either set a function or a resolver, not both.")
_("Either set a function or a resolver, not both.") raise ValidationError(msg)
)
@api.constrains("target", "name") @api.constrains("target", "name")
def _check_target(self): def _check_target(self):

View File

@ -21,7 +21,7 @@ class FieldResolver(models.Model):
""" """
_name = "ir.exports.resolver" _name = "ir.exports.resolver"
_description = "Resolver" _description = "Export Resolver"
name = fields.Char() name = fields.Char()
type = fields.Selection([("field", "Field"), ("global", "Global")]) type = fields.Selection([("field", "Field"), ("global", "Global")])
@ -31,7 +31,7 @@ class FieldResolver(models.Model):
help="\n".join(help_message), help="\n".join(help_message),
) )
def eval(self, param, records): def resolve(self, param, records):
self.ensure_one() self.ensure_one()
result = [] result = []
context = records.env.context context = records.env.context

View File

@ -9,6 +9,8 @@ from odoo import api, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools.translate import _ from odoo.tools.translate import _
from .utils import convert_simple_to_full_parser
class Base(models.AbstractModel): class Base(models.AbstractModel):
@ -19,32 +21,6 @@ class Base(models.AbstractModel):
"""Deduct how to handle a field from its parser.""" """Deduct how to handle a field from its parser."""
return parser_field if isinstance(parser_field, tuple) else (parser_field, None) 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["target"] = 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, "fields": _convert_parser(parser)}
@api.model @api.model
def _jsonify_bad_parser_error(self, field_name): def _jsonify_bad_parser_error(self, field_name):
raise UserError(_("Wrong parser configuration for field: `%s`") % field_name) raise UserError(_("Wrong parser configuration for field: `%s`") % field_name)
@ -59,9 +35,8 @@ class Base(models.AbstractModel):
return self._jsonify_bad_parser_error(field_name) return self._jsonify_bad_parser_error(field_name)
@api.model @api.model
def _jsonify_value(self, record, field, resolver): def _jsonify_value(self, field, value):
# TODO: we should get default by field (eg: char field -> "") """Override this function to support new field types."""
value = record[field.name]
if value is False and field.type != "boolean": if value is False and field.type != "boolean":
value = None value = None
elif field.type == "date": elif field.type == "date":
@ -71,18 +46,26 @@ class Base(models.AbstractModel):
value = fields.Datetime.to_datetime(value) value = fields.Datetime.to_datetime(value)
# Get the timestamp converted to the client's timezone. # Get the timestamp converted to the client's timezone.
# This call also add the tzinfo into the datetime object # This call also add the tzinfo into the datetime object
value = fields.Datetime.context_timestamp(record, value) value = fields.Datetime.context_timestamp(self, value)
value = value.isoformat() value = value.isoformat()
return self._resolve(resolver, field, record)[0] if resolver else value return value
@api.model @api.model
def _resolve(self, resolver, *args): def _add_json_key(self, values, json_key, value):
if isinstance(resolver, int): """To manage defaults, you can use a specific resolver."""
resolver = self.env["ir.exports.resolver"].browse(resolver) key_marshaller = json_key.split("=")
return resolver.eval(*args) key = key_marshaller[0]
marshaller = key_marshaller[1] if len(key_marshaller) > 1 else None
if marshaller == "list": # sublist field
if not values.get(key):
values[key] = []
values[key].append(value)
else:
values[key] = value
@api.model @api.model
def _jsonify_record(self, parser, rec, root): def _jsonify_record(self, parser, rec, root):
"""Jsonify one record (rec). Private function called by jsonify."""
for field in parser: for field in parser:
field_dict, subparser = rec.__parse_field(field) field_dict, subparser = rec.__parse_field(field)
field_name = field_dict["name"] field_name = field_dict["name"]
@ -100,14 +83,11 @@ class Base(models.AbstractModel):
if field.type in ("many2one", "reference"): if field.type in ("many2one", "reference"):
value = value[0] if value else None value = value[0] if value else None
else: else:
value = self._jsonify_value(rec, field, field_dict.get("resolver")) resolver = field_dict.get("resolver")
if json_key.endswith("*"): # sublist field value = rec._jsonify_value(field, rec[field.name])
key = json_key[:-1] value = resolver.resolve(field, rec)[0] if resolver else value
if not root.get(key):
root[key] = [] self._add_json_key(root, json_key, value)
root[key].append(value)
else:
root[json_key] = value
return root return root
def jsonify(self, parser, one=False): def jsonify(self, parser, one=False):
@ -140,7 +120,7 @@ class Base(models.AbstractModel):
if one: if one:
self.ensure_one() self.ensure_one()
if isinstance(parser, list): if isinstance(parser, list):
parser = self.convert_simple_to_full_parser(parser) parser = convert_simple_to_full_parser(parser)
resolver = parser.get("resolver") resolver = parser.get("resolver")
results = [{} for record in self] results = [{} for record in self]
@ -151,5 +131,5 @@ class Base(models.AbstractModel):
for record, json in zip(records, results): for record, json in zip(records, results):
self._jsonify_record(parsers[lang], record, json) self._jsonify_record(parsers[lang], record, json)
results = self._resolve(resolver, results, self) if resolver else results results = resolver.resolve(results, self) if resolver else results
return results[0] if one else results return results[0] if one else results

View File

@ -0,0 +1,34 @@
def convert_simple_to_full_parser(parser):
"""Convert a simple API style parser to a full parser"""
assert isinstance(parser, list)
return {"fields": _convert_parser(parser)}
def _f(f, function=None):
"""Return a dict from the string encoding a field to export.
The : is used as a separator to specify a target, if any.
"""
field_split = f.split(":")
field_dict = {"name": field_split[0]}
if len(field_split) > 1:
field_dict["target"] = field_split[1]
if function:
field_dict["function"] = function
return field_dict
def _convert_parser(parser):
"""Recursively process each list to replace encoding fields as string
by dicts specifying each attribute by its relevant key.
"""
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

View File

@ -53,7 +53,7 @@ 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. Further features are available for advanced uses.
It defines a simple "resolver" model that has a "python_code" field and an eval It defines a simple "resolver" model that has a "python_code" field and a resolve
function so that arbitrary functions can be configured to transform fields, function so that arbitrary functions can be configured to transform fields,
or process the resulting dictionary. or process the resulting dictionary.
It is also to specify a lang to extract the translation of any given field. It is also to specify a lang to extract the translation of any given field.
@ -125,12 +125,12 @@ If the global resolver is given, then the json_dict goes through:
.. code-block:: python .. code-block:: python
resolver.eval(dict, record) resolver.resolve(dict, record)
Which allows to add external data from the context or transform the dictionary 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. if necessary. Similarly if given for a field the resolver evaluates the result.
It is possible for a target to end with a '*': It is possible for a target to have a marshaller by ending the target with '=list':
in that case the result is put into a list. in that case the result is put into a list.
.. code-block:: python .. code-block:: python
@ -138,8 +138,8 @@ in that case the result is put into a list.
parser = { parser = {
fields: [ fields: [
{'name': 'name'}, {'name': 'name'},
{'name': 'field_1', 'target': 'customTags*'}, {'name': 'field_1', 'target': 'customTags=list'},
{'name': 'field_2', 'target': 'customTags*'}, {'name': 'field_2', 'target': 'customTags=list'},
] ]
} }

View File

@ -411,7 +411,7 @@ you can pass a callable or the name of a method on the model:</p>
<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. <p>Further features are available for advanced uses.
It defines a simple “resolver” model that has a “python_code” field and an eval It defines a simple “resolver” model that has a “python_code” field and a resolve
function so that arbitrary functions can be configured to transform fields, function so that arbitrary functions can be configured to transform fields,
or process the resulting dictionary. or process the resulting dictionary.
It is also to specify a lang to extract the translation of any given field.</p> It is also to specify a lang to extract the translation of any given field.</p>
@ -468,18 +468,18 @@ with the added benefit of being able to use resolvers.</p>
<p>A simple parser is simply translated into a full parser at export.</p> <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> <p>If the global resolver is given, then the json_dict goes through:</p>
<pre class="code python literal-block"> <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> <span class="n">resolver</span><span class="o">.</span><span class="n">resolve</span><span class="p">(</span><span class="nb">dict</span><span class="p">,</span> <span class="n">record</span><span class="p">)</span>
</pre> </pre>
<p>Which allows to add external data from the context or transform the dictionary <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> if necessary. Similarly if given for a field the resolver evaluates the result.</p>
<p>It is possible for a target to end with a *: <p>It is possible for a target to have a marshaller by ending the target with =list:
in that case the result is put into a list.</p> in that case the result is put into a list.</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="n">fields</span><span class="p">:</span> <span class="p">[</span> <span class="n">fields</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">'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">'target'</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_1'</span><span class="p">,</span> <span class="s1">'target'</span><span class="p">:</span> <span class="s1">'customTags=list'</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">'target'</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">'target'</span><span class="p">:</span> <span class="s1">'customTags=list'</span><span class="p">},</span>
<span class="p">]</span> <span class="p">]</span>
<span class="p">}</span> <span class="p">}</span>
</pre> </pre>

View File

@ -5,6 +5,8 @@ from odoo import fields
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tests.common import SavepointCase from odoo.tests.common import SavepointCase
from ..models.utils import convert_simple_to_full_parser
def jsonify_custom(self, field_name): def jsonify_custom(self, field_name):
return "yeah!" return "yeah!"
@ -113,7 +115,7 @@ 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()
expected_full_parser = exporter.convert_simple_to_full_parser(expected_parser) expected_full_parser = convert_simple_to_full_parser(expected_parser)
self.assertEqual(parser, expected_full_parser) self.assertEqual(parser, expected_full_parser)
# modify an ir.exports_line to put a target for a field # modify an ir.exports_line to put a target for a field
@ -122,7 +124,7 @@ 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()
expected_full_parser = exporter.convert_simple_to_full_parser(expected_parser) expected_full_parser = convert_simple_to_full_parser(expected_parser)
self.assertEqual(parser, expected_full_parser) self.assertEqual(parser, expected_full_parser)
def test_json_export(self): def test_json_export(self):
@ -251,8 +253,8 @@ class TestParser(SavepointCase):
) )
resolver = self.env["ir.exports.resolver"].create({"python_code": code}) resolver = self.env["ir.exports.resolver"].create({"python_code": code})
lang_parser = [ lang_parser = [
{"target": "customTags*", "name": "name", "resolver": resolver.id}, {"target": "customTags=list", "name": "name", "resolver": resolver},
{"target": "customTags*", "name": "id", "resolver": resolver.id}, {"target": "customTags=list", "name": "id", "resolver": resolver},
] ]
parser = {"language_agnostic": True, "langs": {False: lang_parser}} parser = {"language_agnostic": True, "langs": {False: lang_parser}}
expected_json = { expected_json = {
@ -275,7 +277,7 @@ class TestParser(SavepointCase):
export = self.env["ir.exports"].create( export = self.env["ir.exports"].create(
{ {
"export_fields": [ "export_fields": [
(0, 0, {"name": "name", "function": "jsonify_custom"}), (0, 0, {"name": "name", "instance_method_name": "jsonify_custom"}),
], ],
} }
) )

View File

@ -62,6 +62,6 @@ class TestIrExportsLine(TransactionCase):
"export_id": self.ir_export.id, "export_id": self.ir_export.id,
"name": "name", "name": "name",
"resolver_id": resolver.id, "resolver_id": resolver.id,
"function": "function_name", "instance_method_name": "function_name",
} }
) )

View File

@ -21,7 +21,7 @@
<field name="target" /> <field name="target" />
<field name="lang_id" /> <field name="lang_id" />
<field name="resolver_id" /> <field name="resolver_id" />
<field name="function" /> <field name="instance_method_name" />
</tree> </tree>
</field> </field>
</group> </group>