[REF] base_jsonify: review changes
parent
74a4c2c713
commit
d30ea05fcf
|
@ -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'},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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")]})
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from . import utils
|
||||
from . import models
|
||||
from . import ir_export
|
||||
from . import ir_exports_line
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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'},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"}),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue