diff --git a/base_jsonify/README.rst b/base_jsonify/README.rst index 7dec64196..998fa7212 100644 --- a/base_jsonify/README.rst +++ b/base_jsonify/README.rst @@ -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 * Raphaël Reverdy * Laurent Mignon +* Nans Lefebvre Maintainers ~~~~~~~~~~~ diff --git a/base_jsonify/__manifest__.py b/base_jsonify/__manifest__.py index e225ddbe7..f21c68491 100644 --- a/base_jsonify/__manifest__.py +++ b/base_jsonify/__manifest__.py @@ -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", + ], } diff --git a/base_jsonify/demo/resolver_demo.xml b/base_jsonify/demo/resolver_demo.xml new file mode 100644 index 000000000..540302be2 --- /dev/null +++ b/base_jsonify/demo/resolver_demo.xml @@ -0,0 +1,12 @@ + + + + ExtraData dictionary (number/text) + +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} + + + diff --git a/base_jsonify/models/__init__.py b/base_jsonify/models/__init__.py index f02a8724e..42c938f13 100644 --- a/base_jsonify/models/__init__.py +++ b/base_jsonify/models/__init__.py @@ -1,3 +1,4 @@ from . import models from . import ir_export from . import ir_exports_line +from . import ir_exports_resolver diff --git a/base_jsonify/models/ir_export.py b/base_jsonify/models/ir_export.py index 7165bdf89..3229f1feb 100644 --- a/base_jsonify/models/ir_export.py +++ b/base_jsonify/models/ir_export.py @@ -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 diff --git a/base_jsonify/models/ir_exports_line.py b/base_jsonify/models/ir_exports_line.py index be51c4ab0..25804df4a 100644 --- a/base_jsonify/models/ir_exports_line.py +++ b/base_jsonify/models/ir_exports_line.py @@ -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): diff --git a/base_jsonify/models/ir_exports_resolver.py b/base_jsonify/models/ir_exports_resolver.py new file mode 100644 index 000000000..2df9677a8 --- /dev/null +++ b/base_jsonify/models/ir_exports_resolver.py @@ -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 diff --git a/base_jsonify/models/models.py b/base_jsonify/models/models.py index 5e11a568f..075f8fb71 100644 --- a/base_jsonify/models/models.py +++ b/base_jsonify/models/models.py @@ -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 diff --git a/base_jsonify/readme/CONTRIBUTORS.rst b/base_jsonify/readme/CONTRIBUTORS.rst index 717c76041..a2c786b37 100644 --- a/base_jsonify/readme/CONTRIBUTORS.rst +++ b/base_jsonify/readme/CONTRIBUTORS.rst @@ -1,3 +1,4 @@ * BEAU Sébastien * Raphaël Reverdy * Laurent Mignon +* Nans Lefebvre diff --git a/base_jsonify/readme/DESCRIPTION.rst b/base_jsonify/readme/DESCRIPTION.rst index 93012ac1c..0dfcd34cf 100644 --- a/base_jsonify/readme/DESCRIPTION.rst +++ b/base_jsonify/readme/DESCRIPTION.rst @@ -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. diff --git a/base_jsonify/security/ir.model.access.csv b/base_jsonify/security/ir.model.access.csv new file mode 100644 index 000000000..dcc0a4adf --- /dev/null +++ b/base_jsonify/security/ir.model.access.csv @@ -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 diff --git a/base_jsonify/static/description/index.html b/base_jsonify/static/description/index.html index e2b1e1fda..2c8d50c00 100644 --- a/base_jsonify/static/description/index.html +++ b/base_jsonify/static/description/index.html @@ -371,7 +371,7 @@ ul.auto-toc {

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:

 parser = [
     'name',
@@ -381,8 +381,8 @@ that specify the field to extract.

('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 can define your mapping as follow into the parser definition:

@@ -410,6 +410,62 @@ 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:

+
+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:

+
+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.

+
+parser = {
+    "langs": {
+        False: [
+            {'name': 'name'},
+            {'name': 'field_1', 'alias': 'customTags*'},
+            {'name': 'field_2', 'alias': 'customTags*'},
+        ]
+    }
+}
+
+

Would result in the following json structure:

+
+{
+    '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

diff --git a/base_jsonify/tests/test_get_parser.py b/base_jsonify/tests/test_get_parser.py index 94886b952..ff12aa5a4 100644 --- a/base_jsonify/tests/test_get_parser.py +++ b/base_jsonify/tests/test_get_parser.py @@ -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!"}) diff --git a/base_jsonify/views/ir_exports_resolver_view.xml b/base_jsonify/views/ir_exports_resolver_view.xml new file mode 100644 index 000000000..1f18bcaf0 --- /dev/null +++ b/base_jsonify/views/ir_exports_resolver_view.xml @@ -0,0 +1,26 @@ + + + + ir.exports.resolver + 50 + +
+ + + + + +
+
+
+ + Custom Export Resolvers + ir.exports.resolver + tree,form + + +
diff --git a/base_jsonify/views/ir_exports_view.xml b/base_jsonify/views/ir_exports_view.xml index 9b1dc1d03..075dc6bcc 100644 --- a/base_jsonify/views/ir_exports_view.xml +++ b/base_jsonify/views/ir_exports_view.xml @@ -10,6 +10,8 @@ + + @@ -17,6 +19,9 @@ + + +