diff --git a/base_jsonify/README.rst b/base_jsonify/README.rst index 6f2dffbea..407433303 100644 --- a/base_jsonify/README.rst +++ b/base_jsonify/README.rst @@ -96,16 +96,31 @@ To use these features, a full parser follows the following structure: False: [ {'name': 'description'}, {'name': 'number', 'resolver': 5}, - ({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'display_name'}]) + ({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'display_name'}]) ], 'fr_FR': [ {'name': 'description', 'alias': 'descriptions_fr'}, - ({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'description', 'alias': 'description_fr'}]) + ({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'description', 'alias': 'description_fr'}]) ], } } +One would get the a result having this structure (note that the translated fields are merged in the same dictionary): + +.. code-block:: python + + exported_json == { + "description": "English description", + "description_fr": "French description, voilà", + "number": 42, + "partner": { + "display_name": "partner name", + "description_fr": "French description of that partner", + }, + } + + Note that a resolver can be passed either as a recordset or as an id, so as to be fully serializable. A slightly simpler version in case the translation of fields is not needed, but other features like custom resolvers are: @@ -114,7 +129,6 @@ but other features like custom resolvers are: parser = { "resolver": 3, - "language_agnostic": True, "fields": [ {'name': 'description'}, {'name': 'number', 'resolver': 5}, @@ -123,6 +137,15 @@ but other features like custom resolvers are: } +By passing the `fields` key instead of `langs`, we have essentially the same behaviour as simple parsers, +with the added benefit of being able to use resolvers. + +Standard use-cases of resolvers are: +- give field-specific defaults (e.g. `""` instead of `None`) +- cast a field type (e.g. `int()`) +- alias a particular field for a specific export +- ... + A simple parser is simply translated into a full parser at export. If the global resolver is given, then the json_dict goes through: @@ -135,18 +158,16 @@ 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. +in that case the result is put into a list. .. code-block:: python parser = { - "langs": { - False: [ - {'name': 'name'}, - {'name': 'field_1', 'alias': 'customTags*'}, - {'name': 'field_2', 'alias': 'customTags*'}, - ] - } + fields: [ + {'name': 'name'}, + {'name': 'field_1', 'alias': 'customTags*'}, + {'name': 'field_2', 'alias': 'customTags*'}, + ] } diff --git a/base_jsonify/models/ir_export.py b/base_jsonify/models/ir_export.py index 2804d179e..b41f57825 100644 --- a/base_jsonify/models/ir_export.py +++ b/base_jsonify/models/ir_export.py @@ -7,7 +7,13 @@ from collections import OrderedDict from odoo import fields, models -def partition(l, accessor): # -> Dict +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. + Example: partition([{"name": "ax"}, {"name": "by"}], lambda x: "x" in x["name"]) + => {True: [{"name": "ax"}], False: [{"name": "by"}]} + """ result = {} for item in l: key = accessor(item) diff --git a/base_jsonify/readme/DESCRIPTION.rst b/base_jsonify/readme/DESCRIPTION.rst index 200d13d51..7a64779b1 100644 --- a/base_jsonify/readme/DESCRIPTION.rst +++ b/base_jsonify/readme/DESCRIPTION.rst @@ -69,16 +69,31 @@ To use these features, a full parser follows the following structure: False: [ {'name': 'description'}, {'name': 'number', 'resolver': 5}, - ({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'display_name'}]) + ({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'display_name'}]) ], 'fr_FR': [ {'name': 'description', 'alias': 'descriptions_fr'}, - ({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'description', 'alias': 'description_fr'}]) + ({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'description', 'alias': 'description_fr'}]) ], } } +One would get the a result having this structure (note that the translated fields are merged in the same dictionary): + +.. code-block:: python + + exported_json == { + "description": "English description", + "description_fr": "French description, voilà", + "number": 42, + "partner": { + "display_name": "partner name", + "description_fr": "French description of that partner", + }, + } + + Note that a resolver can be passed either as a recordset or as an id, so as to be fully serializable. A slightly simpler version in case the translation of fields is not needed, but other features like custom resolvers are: @@ -87,7 +102,6 @@ but other features like custom resolvers are: parser = { "resolver": 3, - "language_agnostic": True, "fields": [ {'name': 'description'}, {'name': 'number', 'resolver': 5}, @@ -96,6 +110,15 @@ but other features like custom resolvers are: } +By passing the `fields` key instead of `langs`, we have essentially the same behaviour as simple parsers, +with the added benefit of being able to use resolvers. + +Standard use-cases of resolvers are: +- give field-specific defaults (e.g. `""` instead of `None`) +- cast a field type (e.g. `int()`) +- alias a particular field for a specific export +- ... + A simple parser is simply translated into a full parser at export. If the global resolver is given, then the json_dict goes through: @@ -108,18 +131,16 @@ 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. +in that case the result is put into a list. .. code-block:: python parser = { - "langs": { - False: [ - {'name': 'name'}, - {'name': 'field_1', 'alias': 'customTags*'}, - {'name': 'field_2', 'alias': 'customTags*'}, - ] - } + fields: [ + {'name': 'name'}, + {'name': 'field_1', 'alias': 'customTags*'}, + {'name': 'field_2', 'alias': 'customTags*'}, + ] } diff --git a/base_jsonify/static/description/index.html b/base_jsonify/static/description/index.html index cda1f510a..4eccd00a7 100644 --- a/base_jsonify/static/description/index.html +++ b/base_jsonify/static/description/index.html @@ -424,22 +424,33 @@ It is also to specify a lang to extract the translation of any given field.
False: [ {'name': 'description'}, {'name': 'number', 'resolver': 5}, - ({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'display_name'}]) + ({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'display_name'}]) ], 'fr_FR': [ {'name': 'description', 'alias': 'descriptions_fr'}, - ({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'description', 'alias': 'description_fr'}]) + ({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'description', 'alias': 'description_fr'}]) ], } } +One would get the a result having this structure (note that the translated fields are merged in the same dictionary):
++exported_json == { + "description": "English description", + "description_fr": "French description, voilà", + "number": 42, + "partner": { + "display_name": "partner name", + "description_fr": "French description of that partner", + }, +} +
Note that a resolver can be passed either as a recordset or as an id, so as to be fully serializable. A slightly simpler version in case the translation of fields is not needed, but other features like custom resolvers are:
parser = { "resolver": 3, - "language_agnostic": True, "fields": [ {'name': 'description'}, {'name': 'number', 'resolver': 5}, @@ -447,6 +458,13 @@ but other features like custom resolvers are: ], }+
By passing the fields key instead of langs, we have essentially the same behaviour as simple parsers, +with the added benefit of being able to use resolvers.
+Standard use-cases of resolvers are: +- give field-specific defaults (e.g. “” instead of None) +- cast a field type (e.g. int()) +- alias a particular field for a specific export +- …
A simple parser is simply translated into a full parser at export.
If the global resolver is given, then the json_dict goes through:
@@ -455,16 +473,14 @@ but other features like custom resolvers are: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.
+in that case the result is put into a list.parser = { - "langs": { - False: [ - {'name': 'name'}, - {'name': 'field_1', 'alias': 'customTags*'}, - {'name': 'field_2', 'alias': 'customTags*'}, - ] - } + fields: [ + {'name': 'name'}, + {'name': 'field_1', 'alias': 'customTags*'}, + {'name': 'field_2', 'alias': 'customTags*'}, + ] }Would result in the following json structure:
diff --git a/base_jsonify/tests/test_get_parser.py b/base_jsonify/tests/test_get_parser.py index ff12aa5a4..cf0c025d6 100644 --- a/base_jsonify/tests/test_get_parser.py +++ b/base_jsonify/tests/test_get_parser.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields +from odoo.exceptions import UserError from odoo.tests.common import SavepointCase @@ -281,3 +282,16 @@ class TestParser(SavepointCase): json = self.category.jsonify(export.get_json_parser())[0] self.assertEqual(json, {"name": "yeah!"}) + + def test_bad_parsers(self): + bad_field_name = ["Name"] + with self.assertRaises(KeyError): + self.category.jsonify(bad_field_name, one=True) + + bad_function_name = {"fields": [{"name": "name", "function": "notafunction"}]} + with self.assertRaises(UserError): + self.category.jsonify(bad_function_name, one=True) + + bad_subparser = {"fields": [({"name": "name"}, [{"name": "subparser_name"}])]} + with self.assertRaises(UserError): + self.category.jsonify(bad_subparser, one=True) diff --git a/base_jsonify/tests/test_ir_exports_line.py b/base_jsonify/tests/test_ir_exports_line.py index 8640a2aae..6aab58ee3 100644 --- a/base_jsonify/tests/test_ir_exports_line.py +++ b/base_jsonify/tests/test_ir_exports_line.py @@ -49,3 +49,19 @@ class TestIrExportsLine(TransactionCase): } ) self.assertTrue(line) + + def test_resolver_function_constrains(self): + resolver = self.env["ir.exports.resolver"].create( + {"python_code": "result = value", "type": "field"} + ) + ir_export_lines_model = self.env["ir.exports.line"] + with self.assertRaises(ValidationError): + # the callable should be an existing model function, but it's not checked + ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "name", + "resolver_id": resolver.id, + "function": "function_name", + } + )