[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.
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,
or process the resulting dictionary.
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
resolver.eval(dict, record)
resolver.resolve(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 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.
.. code-block:: python
@ -165,8 +165,8 @@ in that case the result is put into a list.
parser = {
fields: [
{'name': 'name'},
{'name': 'field_1', 'target': 'customTags*'},
{'name': 'field_2', 'target': 'customTags*'},
{'name': 'field_1', 'target': 'customTags=list'},
{'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).
from openupgradelib import openupgrade
def migrate(cr, version):
query = """ALTER TABLE "ir_exports_line" RENAME COLUMN "alias" TO "target";"""
cr.execute(query)
@openupgrade.migrate()
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 ir_export
from . import ir_exports_line

View File

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

View File

@ -24,19 +24,18 @@ class IrExportsLine(models.Model):
string="Custom resolver",
help="If set, will apply the resolver on the field value",
)
function = fields.Char(
instance_method_name = 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")
@api.constrains("resolver_id", "instance_method_name")
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.")
)
if rec.resolver_id and rec.instance_method_name:
msg = _("Either set a function or a resolver, not both.")
raise ValidationError(msg)
@api.constrains("target", "name")
def _check_target(self):

View File

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

View File

@ -9,6 +9,8 @@ from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.translate import _
from .utils import convert_simple_to_full_parser
class Base(models.AbstractModel):
@ -19,32 +21,6 @@ class Base(models.AbstractModel):
"""Deduct how to handle a field from its parser."""
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
def _jsonify_bad_parser_error(self, 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)
@api.model
def _jsonify_value(self, record, field, resolver):
# TODO: we should get default by field (eg: char field -> "")
value = record[field.name]
def _jsonify_value(self, field, value):
"""Override this function to support new field types."""
if value is False and field.type != "boolean":
value = None
elif field.type == "date":
@ -71,18 +46,26 @@ class Base(models.AbstractModel):
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 = fields.Datetime.context_timestamp(self, value)
value = value.isoformat()
return self._resolve(resolver, field, record)[0] if resolver else value
return value
@api.model
def _resolve(self, resolver, *args):
if isinstance(resolver, int):
resolver = self.env["ir.exports.resolver"].browse(resolver)
return resolver.eval(*args)
def _add_json_key(self, values, json_key, value):
"""To manage defaults, you can use a specific resolver."""
key_marshaller = json_key.split("=")
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
def _jsonify_record(self, parser, rec, root):
"""Jsonify one record (rec). Private function called by jsonify."""
for field in parser:
field_dict, subparser = rec.__parse_field(field)
field_name = field_dict["name"]
@ -100,14 +83,11 @@ class Base(models.AbstractModel):
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
resolver = field_dict.get("resolver")
value = rec._jsonify_value(field, rec[field.name])
value = resolver.resolve(field, rec)[0] if resolver else value
self._add_json_key(root, json_key, value)
return root
def jsonify(self, parser, one=False):
@ -140,7 +120,7 @@ class Base(models.AbstractModel):
if one:
self.ensure_one()
if isinstance(parser, list):
parser = self.convert_simple_to_full_parser(parser)
parser = convert_simple_to_full_parser(parser)
resolver = parser.get("resolver")
results = [{} for record in self]
@ -151,5 +131,5 @@ class Base(models.AbstractModel):
for record, json in zip(records, results):
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

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.
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,
or process the resulting dictionary.
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
resolver.eval(dict, record)
resolver.resolve(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 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.
.. code-block:: python
@ -138,8 +138,8 @@ in that case the result is put into a list.
parser = {
fields: [
{'name': 'name'},
{'name': 'field_1', 'target': 'customTags*'},
{'name': 'field_2', 'target': 'customTags*'},
{'name': 'field_1', 'target': 'customTags=list'},
{'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
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
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,
or process the resulting dictionary.
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>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>
<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>
<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 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>
<pre class="code python literal-block">
<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="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_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_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=list'</span><span class="p">},</span>
<span class="p">]</span>
<span class="p">}</span>
</pre>

View File

@ -5,6 +5,8 @@ from odoo import fields
from odoo.exceptions import UserError
from odoo.tests.common import SavepointCase
from ..models.utils import convert_simple_to_full_parser
def jsonify_custom(self, field_name):
return "yeah!"
@ -113,7 +115,7 @@ class TestParser(SavepointCase):
exporter = self.env.ref("base_jsonify.ir_exp_partner")
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)
# 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"])
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)
def test_json_export(self):
@ -251,8 +253,8 @@ class TestParser(SavepointCase):
)
resolver = self.env["ir.exports.resolver"].create({"python_code": code})
lang_parser = [
{"target": "customTags*", "name": "name", "resolver": resolver.id},
{"target": "customTags*", "name": "id", "resolver": resolver.id},
{"target": "customTags=list", "name": "name", "resolver": resolver},
{"target": "customTags=list", "name": "id", "resolver": resolver},
]
parser = {"language_agnostic": True, "langs": {False: lang_parser}}
expected_json = {
@ -275,7 +277,7 @@ class TestParser(SavepointCase):
export = self.env["ir.exports"].create(
{
"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,
"name": "name",
"resolver_id": resolver.id,
"function": "function_name",
"instance_method_name": "function_name",
}
)

View File

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