From a997125dc90384f8a19b8115ac5670a20ae8ac19 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 20 Apr 2022 13:38:15 +0200 Subject: [PATCH 01/20] Add jsonifier (skeleton) --- jsonifier/README.rst | 3 +++ jsonifier/__init__.py | 0 jsonifier/__manifest__.py | 16 ++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 jsonifier/README.rst create mode 100644 jsonifier/__init__.py create mode 100644 jsonifier/__manifest__.py diff --git a/jsonifier/README.rst b/jsonifier/README.rst new file mode 100644 index 000000000..8f2cd2a5f --- /dev/null +++ b/jsonifier/README.rst @@ -0,0 +1,3 @@ +This module is meant to replace `base_jsonify`. +Once is confirmed that it can be published on the apps store +the switch will be made. diff --git a/jsonifier/__init__.py b/jsonifier/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/jsonifier/__manifest__.py b/jsonifier/__manifest__.py new file mode 100644 index 000000000..c2ad800c5 --- /dev/null +++ b/jsonifier/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2017-2018 Akretion (http://www.akretion.com) +# Sébastien BEAU +# Raphaël Reverdy +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +{ + "name": "JSONifier", + "summary": "JSON-ify data for all models - SKELETON", + "version": "14.0.0.1.0", + "category": "Uncategorized", + "website": "https://github.com/OCA/server-tools", + "author": "Akretion, ACSONE, Camptocamp, Odoo Community Association (OCA)", + "license": "LGPL-3", + "installable": True, + "depends": ["base"], +} From aff1784199027d23d7eb46e31e1c290f27bf0519 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 22 Apr 2022 06:40:31 +0000 Subject: [PATCH 02/20] [ADD] icon.png --- jsonifier/static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 jsonifier/static/description/icon.png diff --git a/jsonifier/static/description/icon.png b/jsonifier/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From 4175d5cefa36b57f66ea9ed8763fda41b7fd9991 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 20 Apr 2022 13:31:57 +0200 Subject: [PATCH 03/20] Replace base_jsonify w/ jsonifier Rationale: 1. the name reflects better what it does as this is not _just_ a base module 2. solves publication issue on odoo apps store due to an old module registered w/ the same name that even if un-published blocks publishing the module for all versions. This in turn, blocks the publication of ALL modules that depend on base_jsonify. --- jsonifier/README.rst | 4 +- jsonifier/__init__.py | 1 + jsonifier/__manifest__.py | 14 +- jsonifier/demo/export_demo.xml | 7 + jsonifier/demo/ir.exports.line.csv | 16 + jsonifier/demo/resolver_demo.xml | 12 + jsonifier/i18n/ca.po | 229 ++++++++ jsonifier/i18n/zh_CN.po | 231 ++++++++ jsonifier/models/__init__.py | 5 + jsonifier/models/ir_exports.py | 124 +++++ jsonifier/models/ir_exports_line.py | 59 ++ jsonifier/models/ir_exports_resolver.py | 53 ++ jsonifier/models/models.py | 210 +++++++ jsonifier/models/utils.py | 35 ++ jsonifier/readme/CONTRIBUTORS.rst | 6 + jsonifier/readme/DESCRIPTION.rst | 166 ++++++ jsonifier/security/ir.model.access.csv | 2 + jsonifier/static/description/index.html | 550 +++++++++++++++++++ jsonifier/tests/__init__.py | 3 + jsonifier/tests/test_get_parser.py | 352 ++++++++++++ jsonifier/tests/test_helpers.py | 45 ++ jsonifier/tests/test_ir_exports_line.py | 68 +++ jsonifier/views/ir_exports_resolver_view.xml | 26 + jsonifier/views/ir_exports_view.xml | 38 ++ 24 files changed, 2251 insertions(+), 5 deletions(-) create mode 100644 jsonifier/demo/export_demo.xml create mode 100644 jsonifier/demo/ir.exports.line.csv create mode 100644 jsonifier/demo/resolver_demo.xml create mode 100644 jsonifier/i18n/ca.po create mode 100644 jsonifier/i18n/zh_CN.po create mode 100644 jsonifier/models/__init__.py create mode 100644 jsonifier/models/ir_exports.py create mode 100644 jsonifier/models/ir_exports_line.py create mode 100644 jsonifier/models/ir_exports_resolver.py create mode 100644 jsonifier/models/models.py create mode 100644 jsonifier/models/utils.py create mode 100644 jsonifier/readme/CONTRIBUTORS.rst create mode 100644 jsonifier/readme/DESCRIPTION.rst create mode 100644 jsonifier/security/ir.model.access.csv create mode 100644 jsonifier/static/description/index.html create mode 100644 jsonifier/tests/__init__.py create mode 100644 jsonifier/tests/test_get_parser.py create mode 100644 jsonifier/tests/test_helpers.py create mode 100644 jsonifier/tests/test_ir_exports_line.py create mode 100644 jsonifier/views/ir_exports_resolver_view.xml create mode 100644 jsonifier/views/ir_exports_view.xml diff --git a/jsonifier/README.rst b/jsonifier/README.rst index 8f2cd2a5f..89bcd6c21 100644 --- a/jsonifier/README.rst +++ b/jsonifier/README.rst @@ -1,3 +1 @@ -This module is meant to replace `base_jsonify`. -Once is confirmed that it can be published on the apps store -the switch will be made. +wait for the bot ;) diff --git a/jsonifier/__init__.py b/jsonifier/__init__.py index e69de29bb..0650744f6 100644 --- a/jsonifier/__init__.py +++ b/jsonifier/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/jsonifier/__manifest__.py b/jsonifier/__manifest__.py index c2ad800c5..ec1a7d944 100644 --- a/jsonifier/__manifest__.py +++ b/jsonifier/__manifest__.py @@ -5,12 +5,22 @@ { "name": "JSONifier", - "summary": "JSON-ify data for all models - SKELETON", - "version": "14.0.0.1.0", + "summary": "JSON-ify data for all models", + "version": "14.0.1.0.0", "category": "Uncategorized", "website": "https://github.com/OCA/server-tools", "author": "Akretion, ACSONE, Camptocamp, Odoo Community Association (OCA)", "license": "LGPL-3", "installable": True, "depends": ["base"], + "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/jsonifier/demo/export_demo.xml b/jsonifier/demo/export_demo.xml new file mode 100644 index 000000000..a060d3002 --- /dev/null +++ b/jsonifier/demo/export_demo.xml @@ -0,0 +1,7 @@ + + + + Partner Export + res.partner + + diff --git a/jsonifier/demo/ir.exports.line.csv b/jsonifier/demo/ir.exports.line.csv new file mode 100644 index 000000000..476de3937 --- /dev/null +++ b/jsonifier/demo/ir.exports.line.csv @@ -0,0 +1,16 @@ +id,export_id/id,name +name,ir_exp_partner,name +active,ir_exp_partner,active +credit_limit,ir_exp_partner,credit_limit +color,ir_exp_partner,color +category_id_name,ir_exp_partner,category_id/name +country_id_name,ir_exp_partner,country_id/name +country_id_code,ir_exp_partner,country_id/code +child_ids_name,ir_exp_partner,child_ids/name +child_ids_id,ir_exp_partner,child_ids/id +child_ids_email,ir_exp_partner,child_ids/email +child_ids_country_id_name,ir_exp_partner,child_ids/country_id/name +child_ids_country_id_code,ir_exp_partner,child_ids/country_id/code +child_ids_child_ids_name,ir_exp_partner,child_ids/child_ids/name +lang,ir_exp_partner,lang +comment,ir_exp_partner,comment diff --git a/jsonifier/demo/resolver_demo.xml b/jsonifier/demo/resolver_demo.xml new file mode 100644 index 000000000..540302be2 --- /dev/null +++ b/jsonifier/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/jsonifier/i18n/ca.po b/jsonifier/i18n/ca.po new file mode 100644 index 000000000..f7c8cd680 --- /dev/null +++ b/jsonifier/i18n/ca.po @@ -0,0 +1,229 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * jsonifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__instance_method_name +msgid "A method defined on the model that takes a record and a field_name" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__active +msgid "Active" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_base +msgid "Base" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_resolver__python_code +msgid "" +"Compute the result from 'value' by setting the variable 'result'.\n" +"For fields resolvers:\n" +":param name: name of the field\n" +":param value: value of the field\n" +":param field_type: type of the field\n" +"For global resolvers:\n" +":param value: JSON dict\n" +":param record: the record" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Configuration" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_uid +msgid "Created by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_date +msgid "Created on" +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_resolver_view +#: model:ir.ui.menu,name:jsonifier.ui_exports_resolvers +msgid "Custom Export Resolvers" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__global_resolver_id +msgid "Custom global resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__resolver_id +msgid "Custom resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__display_name +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__display_name +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name +msgid "Display Name" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "Either set a function or a resolver, not both." +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_view +#: model:ir.ui.menu,name:jsonifier.ui_exports +msgid "Export Fields" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_resolver +msgid "Export Resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports +msgid "Exports" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_line +msgid "Exports Line" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__field +msgid "Field" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__instance_method_name +msgid "Function" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__global +msgid "Global" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__id +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__id +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id +msgid "ID" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__lang_id +msgid "If set, the language in which the field is exported" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__global_resolver_id +msgid "If set, will apply the global resolver to the result" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__resolver_id +msgid "If set, will apply the resolver on the field value" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__language_agnostic +msgid "" +"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" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Index" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__lang_id +msgid "Language" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__language_agnostic +msgid "Language Agnostic" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports____last_update +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line____last_update +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver____last_update +msgid "Last Modified on" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_date +msgid "Last Updated on" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__name +msgid "Name" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "Name and Target must have the same hierarchy depth" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__python_code +msgid "Python Code" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target +msgid "Target" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target +msgid "" +"The complete path to the field where you can specify a target on the step as" +" field:target" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "The target must reference the same field as in name '%s' not in '%s'" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type +msgid "Type" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/models.py:0 +#, python-format +msgid "Wrong parser configuration for field: `%s`" +msgstr "" diff --git a/jsonifier/i18n/zh_CN.po b/jsonifier/i18n/zh_CN.po new file mode 100644 index 000000000..407cdaf86 --- /dev/null +++ b/jsonifier/i18n/zh_CN.po @@ -0,0 +1,231 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * jsonifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2019-08-31 04:35+0000\n" +"Last-Translator: 黎伟杰 <674416404@qq.com>\n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 3.8\n" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__instance_method_name +msgid "A method defined on the model that takes a record and a field_name" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__active +msgid "Active" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_base +msgid "Base" +msgstr "基础" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_resolver__python_code +msgid "" +"Compute the result from 'value' by setting the variable 'result'.\n" +"For fields resolvers:\n" +":param name: name of the field\n" +":param value: value of the field\n" +":param field_type: type of the field\n" +"For global resolvers:\n" +":param value: JSON dict\n" +":param record: the record" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Configuration" +msgstr "配置" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_uid +msgid "Created by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_date +msgid "Created on" +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_resolver_view +#: model:ir.ui.menu,name:jsonifier.ui_exports_resolvers +msgid "Custom Export Resolvers" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__global_resolver_id +msgid "Custom global resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__resolver_id +msgid "Custom resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__display_name +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__display_name +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name +msgid "Display Name" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "Either set a function or a resolver, not both." +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_view +#: model:ir.ui.menu,name:jsonifier.ui_exports +msgid "Export Fields" +msgstr "导出字段" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_resolver +msgid "Export Resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports +msgid "Exports" +msgstr "导出" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_line +msgid "Exports Line" +msgstr "导出行" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__field +msgid "Field" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__instance_method_name +msgid "Function" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__global +msgid "Global" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__id +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__id +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id +msgid "ID" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__lang_id +msgid "If set, the language in which the field is exported" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__global_resolver_id +msgid "If set, will apply the global resolver to the result" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__resolver_id +msgid "If set, will apply the resolver on the field value" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__language_agnostic +msgid "" +"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" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Index" +msgstr "索引" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__lang_id +msgid "Language" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__language_agnostic +msgid "Language Agnostic" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports____last_update +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line____last_update +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver____last_update +msgid "Last Modified on" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_date +msgid "Last Updated on" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__name +msgid "Name" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "Name and Target must have the same hierarchy depth" +msgstr "名称和别名必须具有相同的层次结构深度" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__python_code +msgid "Python Code" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target +msgid "Target" +msgstr "别名" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target +msgid "" +"The complete path to the field where you can specify a target on the step as " +"field:target" +msgstr "字段的完整路径,您可以在其中指定步骤作为字段的别名:别名" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "The target must reference the same field as in name '%s' not in '%s'" +msgstr "别名必须引用与名称相同的字段'%s'不在'%s'" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type +msgid "Type" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/models.py:0 +#, fuzzy, python-format +msgid "Wrong parser configuration for field: `%s`" +msgstr "错误的解析器配置" diff --git a/jsonifier/models/__init__.py b/jsonifier/models/__init__.py new file mode 100644 index 000000000..cd8aff409 --- /dev/null +++ b/jsonifier/models/__init__.py @@ -0,0 +1,5 @@ +from . import utils +from . import models +from . import ir_exports +from . import ir_exports_line +from . import ir_exports_resolver diff --git a/jsonifier/models/ir_exports.py b/jsonifier/models/ir_exports.py new file mode 100644 index 000000000..362f3cd10 --- /dev/null +++ b/jsonifier/models/ir_exports.py @@ -0,0 +1,124 @@ +# © 2017 Akretion (http://www.akretion.com) +# Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +from collections import OrderedDict + +from odoo import fields, models +from odoo.tools import ormcache + + +def partition(line, accessor): + """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"}]} + """ + result = {} + for item in line: + 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: + + { + "name": True, + "resource": True, + } + + Order of keys is important. + """ + field = fields[0] + if len(fields) == 1: + if field == ".id": + field = "id" + data[field] = (True, options) + else: + if field not in data: + data[field] = (False, OrderedDict()) + update_dict(data[field][1], fields[1:], options) + + +def convert_dict(dict_parser): + """Convert dict returned by update_dict to list consistent w/ Odoo API. + + The list is composed of strings (field names or targets) or tuples. + """ + parser = [] + for field, value in dict_parser.items(): + if value[0] is True: # is a leaf + parser.append(field_dict(field, value[1])) + else: + parser.append((field_dict(field), convert_dict(value[1]))) + return 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] + for option in options or {}: + if options[option]: + result[option] = options[option] + return result + + +class IrExports(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", + ) + + @ormcache( + "self.language_agnostic", + "self.global_resolver_id.id", + "tuple(self.export_fields.mapped('write_date'))", + ) + 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() + parser = {} + 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.target: + names = line.target.split("/") + 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]: + parser["fields"] = lang_parsers[False] + else: + 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 diff --git a/jsonifier/models/ir_exports_line.py b/jsonifier/models/ir_exports_line.py new file mode 100644 index 000000000..f45c0e7f5 --- /dev/null +++ b/jsonifier/models/ir_exports_line.py @@ -0,0 +1,59 @@ +# Copyright 2017 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class IrExportsLine(models.Model): + _inherit = "ir.exports.line" + + target = fields.Char( + "Target", + help="The complete path to the field where you can specify a " + "target on the step as field:target", + ) + 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", + ) + instance_method_name = fields.Char( + string="Function", + help="A method defined on the model that takes a record and a field_name", + ) + + @api.constrains("resolver_id", "instance_method_name") + def _check_function_resolver(self): + for rec in self: + 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): + for rec in self: + if not rec.target: + continue + names = rec.name.split("/") + names_with_target = rec.target.split("/") + if len(names) != len(names_with_target): + raise ValidationError( + _("Name and Target must have the same hierarchy depth") + ) + for name, name_with_target in zip(names, names_with_target): + field_name = name_with_target.split(":")[0] + if name != field_name: + raise ValidationError( + _( + "The target must reference the same field as in " + "name '%s' not in '%s'" + ) + % (name, name_with_target) + ) diff --git a/jsonifier/models/ir_exports_resolver.py b/jsonifier/models/ir_exports_resolver.py new file mode 100644 index 000000000..81f9ec640 --- /dev/null +++ b/jsonifier/models/ir_exports_resolver.py @@ -0,0 +1,53 @@ +# Copyright 2020 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +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 = "Export Resolver" + + name = fields.Char() + type = fields.Selection([("field", "Field"), ("global", "Global")]) + python_code = fields.Text( + string="Python Code", + default="\n".join(["# " + h for h in help_message] + ["result = value"]), + help="\n".join(help_message), + ) + + def resolve(self, param, records): + self.ensure_one() + result = [] + context = records.env.context + if self.type == "global": + assert len(param) == len(records) + for value, record in zip(param, records): + values = {"value": value, "record": record, "context": context} + 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, + "context": context, + } + safe_eval(self.python_code, values, mode="exec", nocopy=True) + result.append(values["result"]) + return result diff --git a/jsonifier/models/models.py b/jsonifier/models/models.py new file mode 100644 index 000000000..226f771ab --- /dev/null +++ b/jsonifier/models/models.py @@ -0,0 +1,210 @@ +# Copyright 2017 Akretion (http://www.akretion.com) +# Sébastien BEAU +# Raphaël Reverdy +# Copyright 2020 Camptocamp SA (http://www.camptocamp.com) +# Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). + +import logging + +from odoo import api, fields, models, tools +from odoo.exceptions import UserError +from odoo.tools.misc import format_duration +from odoo.tools.translate import _ + +from .utils import convert_simple_to_full_parser + +_logger = logging.getLogger(__name__) + + +class Base(models.AbstractModel): + + _inherit = "base" + + @api.model + def __parse_field(self, parser_field): + """Deduct how to handle a field from its parser.""" + return parser_field if isinstance(parser_field, tuple) else (parser_field, None) + + @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(record, 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, field, value): + """Override this function to support new field types.""" + 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"): + value = value.display_name if value else None + elif field.type in ("one2many", "many2many"): + value = [v.display_name for v in value] + return value + + @api.model + def _add_json_key(self, values, json_key, value): + """To manage defaults, you can use a specific resolver.""" + key, sep, marshaller = json_key.partition("=") + 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.""" + strict = self.env.context.get("jsonify_record_strict", False) + for field in parser: + field_dict, subparser = rec.__parse_field(field) + field_name = field_dict["name"] + if field_name not in rec._fields: + if strict: + # let it fail + rec._fields[field_name] # pylint: disable=pointless-statement + if not tools.config["test_enable"]: + # If running live, log proper error + # so that techies can track it down + _logger.error( + "%(model)s.%(fname)s not available", + {"model": self._name, "fname": field_name}, + ) + continue + json_key = field_dict.get("target", field_name) + field = rec._fields[field_name] + if field_dict.get("function"): + function = field_dict["function"] + try: + value = self._function_value(rec, function, field_name) + except UserError: + if strict: + raise + if not tools.config["test_enable"]: + _logger.error( + "%(model)s.%(func)s not available", + {"model": self._name, "func": str(function)}, + ) + continue + elif subparser: + if not (field.relational or field.type == "reference"): + if strict: + self._jsonify_bad_parser_error(field_name) + if not tools.config["test_enable"]: + _logger.error( + "%(model)s.%(fname)s not relational", + {"model": self._name, "fname": field_name}, + ) + continue + 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: + 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): + """Convert the record according to the given parser. + + Example of (simple) parser: + parser = [ + 'name', + 'number', + 'create_date', + ('partner_id', ['id', 'display_name', 'ref']) + ('shipping_id', callable) + ('delivery_id', "record_method") + ('line_id', ['id', ('product_id', ['name']), 'price_unit']) + ] + + In order to be consistent with the Odoo API the jsonify method always + returns a list of objects even if there is only one element in input. + You can change this behavior by passing `one=True` to get only one element. + + 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: + + parser = [ + 'field_name:json_key' + ] + + """ + if one: + self.ensure_one() + if isinstance(parser, list): + parser = convert_simple_to_full_parser(parser) + resolver = parser.get("resolver") + + results = [{} for record in self] + parsers = {False: parser["fields"]} if "fields" in parser else parser["langs"] + for lang in parsers: + translate = lang or parser.get("language_agnostic") + records = self.with_context(lang=lang) if translate else self + for record, json in zip(records, results): + self._jsonify_record(parsers[lang], record, json) + + if resolver: + results = resolver.resolve(results, self) + return results[0] if one else results + + # HELPERS + + def _jsonify_m2o_to_id(self, fname): + """Helper to get an ID only from a m2o field. + + Example: + + m2o_id + m2o_id:rel_id + _jsonify_m2o_to_id + + """ + return self[fname].id + + def _jsonify_x2m_to_ids(self, fname): + """Helper to get a list of IDs only from a o2m or m2m field. + + Example: + + m2m_ids + m2m_ids:rel_ids + _jsonify_x2m_to_ids + + """ + return self[fname].ids + + def _jsonify_format_duration(self, fname): + """Helper to format a Float-like duration to string 00:00. + + Example: + + duration + _jsonify_format_duration + + """ + return format_duration(self[fname]) diff --git a/jsonifier/models/utils.py b/jsonifier/models/utils.py new file mode 100644 index 000000000..dba45cc06 --- /dev/null +++ b/jsonifier/models/utils.py @@ -0,0 +1,35 @@ +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 _convert_field(fld, 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. + """ + name, sep, target = fld.partition(":") + field_dict = {"name": name} + if target: + field_dict["target"] = target + if function: + field_dict["function"] = function + return field_dict + + +def _convert_parser(parser): + """Recursively process each list to replace encoded fields as string + by dicts specifying each attribute by its relevant key. + """ + result = [] + for line in parser: + if isinstance(line, str): + field_def = _convert_field(line) + else: + fld, sub = line + if callable(sub) or isinstance(sub, str): + field_def = _convert_field(fld, sub) + else: + field_def = (_convert_field(fld), _convert_parser(sub)) + result.append(field_def) + return result diff --git a/jsonifier/readme/CONTRIBUTORS.rst b/jsonifier/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..dc005d8a6 --- /dev/null +++ b/jsonifier/readme/CONTRIBUTORS.rst @@ -0,0 +1,6 @@ +* BEAU Sébastien +* Raphaël Reverdy +* Laurent Mignon +* Nans Lefebvre +* Simone Orsi +* Iván Todorovich diff --git a/jsonifier/readme/DESCRIPTION.rst b/jsonifier/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ca534186a --- /dev/null +++ b/jsonifier/readme/DESCRIPTION.rst @@ -0,0 +1,166 @@ +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 a simple parser: + + +.. code-block:: python + + parser = [ + 'name', + 'number', + 'create_date', + ('partner_id', ['id', 'display_name', 'ref']) + ('line_id', ['id', ('product_id', ['name']), 'price_unit']) + ] + +In order to be consistent with the Odoo API the jsonify method always +returns a list of objects 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: + +.. code-block:: python + + parser = [ + 'field_name:json_key' + ] + +.. code-block:: python + + + parser = [ + 'name', + 'number', + 'create_date:creationDate', + ('partner_id:partners', ['id', 'display_name', 'ref']) + ('line_id:lines', ['id', ('product_id', ['name']), 'price_unit']) + ] + +If you need to parse the value of a field in a custom way, +you can pass a callable or the name of a method on the model: + +.. code-block:: python + + parser = [ + ('name', "jsonify_name") # method name + ('number', lambda rec, field_name: rec[field_name] * 2)) # callable + ] + +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 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. + +To use these features, a full parser follows the following structure: + +.. code-block:: python + + parser = { + "resolver": 3, + "language_agnostic": True, + "langs": { + False: [ + {'name': 'description'}, + {'name': 'number', 'resolver': 5}, + ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'display_name'}]) + ], + 'fr_FR': [ + {'name': 'description', 'target': 'descriptions_fr'}, + ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'description', 'target': 'description_fr'}]) + ], + } + } + + +One would get 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: + +.. code-block:: python + + parser = { + "resolver": 3, + "fields": [ + {'name': 'description'}, + {'name': 'number', 'resolver': 5}, + ({'name': 'partner_id', 'target': 'partners'}, [{'name': 'display_name'}]), + ], + } + + +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: + +.. code-block:: python + + 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 have a marshaller by ending the target with '=list': +in that case the result is put into a list. + +.. code-block:: python + + parser = { + fields: [ + {'name': 'name'}, + {'name': 'field_1', 'target': 'customTags=list'}, + {'name': 'field_2', 'target': 'customTags=list'}, + ] + } + + +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. + + +NOTE: this module was named `base_jsonify` till version 14.0.1.5.0. diff --git a/jsonifier/security/ir.model.access.csv b/jsonifier/security/ir.model.access.csv new file mode 100644 index 000000000..dcc0a4adf --- /dev/null +++ b/jsonifier/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/jsonifier/static/description/index.html b/jsonifier/static/description/index.html new file mode 100644 index 000000000..a48dbab00 --- /dev/null +++ b/jsonifier/static/description/index.html @@ -0,0 +1,550 @@ + + + + + + +Base JSONify + + + +
+

Base JSONify

+ + +

Beta License: LGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

+

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 a simple parser:

+
+parser = [
+    'name',
+    'number',
+    'create_date',
+    ('partner_id', ['id', 'display_name', 'ref'])
+    ('line_id', ['id', ('product_id', ['name']), 'price_unit'])
+]
+
+

In order to be consistent with the Odoo API the jsonify method always +returns a list of objects 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:

+
+parser = [
+    'field_name:json_key'
+]
+
+
+parser = [
+    'name',
+    'number',
+    'create_date:creationDate',
+    ('partner_id:partners', ['id', 'display_name', 'ref'])
+    ('line_id:lines', ['id', ('product_id', ['name']), 'price_unit'])
+]
+
+

If you need to parse the value of a field in a custom way, +you can pass a callable or the name of a method on the model:

+
+parser = [
+    ('name', "jsonify_name")  # method name
+    ('number', lambda rec, field_name: rec[field_name] * 2))  # callable
+]
+
+

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

+

To use these features, a full parser follows the following structure:

+
+parser = {
+    "resolver": 3,
+    "language_agnostic": True,
+    "langs": {
+        False: [
+            {'name': 'description'},
+            {'name': 'number', 'resolver': 5},
+            ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'display_name'}])
+        ],
+        'fr_FR': [
+            {'name': 'description', 'target': 'descriptions_fr'},
+            ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'description', 'target': 'description_fr'}])
+        ],
+    }
+}
+
+

One would get 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,
+    "fields": [
+            {'name': 'description'},
+            {'name': 'number', 'resolver': 5},
+            ({'name': 'partner_id', 'target': 'partners'}, [{'name': 'display_name'}]),
+    ],
+}
+
+

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:

+
+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 have a marshaller by ending the target with ‘=list’: +in that case the result is put into a list.

+
+parser = {
+    fields: [
+        {'name': 'name'},
+        {'name': 'field_1', 'target': 'customTags=list'},
+        {'name': 'field_2', 'target': 'customTags=list'},
+    ]
+}
+
+

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

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/jsonifier/tests/__init__.py b/jsonifier/tests/__init__.py new file mode 100644 index 000000000..3402bb152 --- /dev/null +++ b/jsonifier/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_get_parser +from . import test_helpers +from . import test_ir_exports_line diff --git a/jsonifier/tests/test_get_parser.py b/jsonifier/tests/test_get_parser.py new file mode 100644 index 000000000..57f060aa5 --- /dev/null +++ b/jsonifier/tests/test_get_parser.py @@ -0,0 +1,352 @@ +# Copyright 2017 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import mock + +from odoo import fields, tools +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!" + + +class TestParser(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # disable tracking test suite wise + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.env.user.tz = "Europe/Brussels" + cls.partner = cls.env["res.partner"].create( + { + "name": "Akretion", + "country_id": cls.env.ref("base.fr").id, + "lang": "en_US", # default + "category_id": [(0, 0, {"name": "Inovator"})], + "child_ids": [ + ( + 0, + 0, + { + "name": "Sebatien Beau", + "country_id": cls.env.ref("base.fr").id, + }, + ) + ], + "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_target = "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_target, + "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", + "target": "name:{}".format(cls.translated_target), + "lang_id": cls.lang.id, + }, + ), + ( + 0, + 0, + { + "name": "name", + "target": "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 = [ + "name", + "active", + "credit_limit", + "color", + ("category_id", ["name"]), + ("country_id", ["name", "code"]), + ( + "child_ids", + [ + "name", + "id", + "email", + ("country_id", ["name", "code"]), + ("child_ids", ["name"]), + ], + ), + "lang", + "comment", + ] + + exporter = self.env.ref("jsonifier.ir_exp_partner") + parser = exporter.get_json_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 + self.env.ref("jsonifier.category_id_name").write( + {"target": "category_id:category/name"} + ) + expected_parser[4] = ("category_id:category", ["name"]) + parser = exporter.get_json_parser() + expected_full_parser = 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 + parser = [ + "lang", + "comment", + "credit_limit", + "name", + "color", + ( + "child_ids:children", + [ + ("child_ids:children", ["name"]), + "email", + ("country_id:country", ["code", "name"]), + "name", + "id", + ], + ), + ("country_id:country", ["code", "name"]), + "active", + ("category_id", ["name"]), + "create_date", + "date", + ] + # put our own create date to ease tests + self.env.cr.execute( + "update res_partner set create_date=%s where id=%s", + ("2019-10-31 14:39:49", self.partner.id), + ) + expected_json = { + "lang": "en_US", + "comment": None, + "credit_limit": 0.0, + "name": "Akretion", + "color": 0, + "country": {"code": "FR", "name": "France"}, + "active": True, + "category_id": [{"name": "Inovator"}], + "children": [ + { + "id": self.partner.child_ids.id, + "country": {"code": "FR", "name": "France"}, + "children": [], + "name": "Sebatien Beau", + "email": None, + } + ], + "create_date": "2019-10-31T15:39:49+01:00", + "date": "2019-10-31", + } + json_partner = self.partner.jsonify(parser) + + self.assertDictEqual(json_partner[0], expected_json) + + # Check that only boolean fields have boolean values into json + # By default if a field is not set into Odoo, the value is always False + # This value is not the expected one into the json + self.partner.write({"child_ids": [(6, 0, [])], "active": False, "lang": False}) + json_partner = self.partner.jsonify(parser) + expected_json["active"] = False + expected_json["lang"] = None + expected_json["children"] = [] + self.assertDictEqual(json_partner[0], expected_json) + + def test_one(self): + parser = [ + "name", + ] + expected_json = { + "name": "Akretion", + } + json_partner = self.partner.jsonify(parser, one=True) + self.assertDictEqual(json_partner, expected_json) + # cannot call on multiple records + with self.assertRaises(ValueError) as err: + self.env["res.partner"].search([]).jsonify(parser, one=True) + self.assertIn("Expected singleton", str(err.exception)) + + def test_json_export_callable_parser(self): + self.partner.__class__.jsonify_custom = jsonify_custom + parser = [ + # callable subparser + ("name", lambda rec, fname: rec[fname] + " rocks!"), + ("name:custom", "jsonify_custom"), + ] + expected_json = { + "name": "Akretion rocks!", + "custom": "yeah!", + } + 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_target], self.translated_target) + 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_target) + + def test_simple_star_target_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 = [ + {"target": "customTags=list", "name": "name", "resolver": resolver}, + {"target": "customTags=list", "name": "id", "resolver": resolver}, + ] + 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", "instance_method_name": "jsonify_custom"}), + ], + } + ) + + json = self.category.jsonify(export.get_json_parser())[0] + self.assertEqual(json, {"name": "yeah!"}) + + def test_export_relational_display_names(self): + """If we export a relational, we get its display_name in the json.""" + parser = [ + "state_id", + "country_id", + "category_id", + "user_ids", + ] + expected_json = { + "state_id": None, + "country_id": "France", + "category_id": ["Inovator"], + "user_ids": [], + } + + json_partner = self.partner.jsonify(parser, one=True) + + self.assertDictEqual(json_partner, expected_json) + + def test_export_reference_display_names(self): + """Reference work the same as relational""" + menu = self.env.ref("base.menu_action_res_users") + + json_menu = menu.jsonify(["action"], one=True) + + self.assertDictEqual(json_menu, {"action": "Users"}) + + def test_bad_parsers_strict(self): + rec = self.category.with_context(jsonify_record_strict=True) + bad_field_name = ["Name"] + with self.assertRaises(KeyError): + rec.jsonify(bad_field_name, one=True) + + bad_function_name = {"fields": [{"name": "name", "function": "notafunction"}]} + with self.assertRaises(UserError): + rec.jsonify(bad_function_name, one=True) + + bad_subparser = {"fields": [({"name": "name"}, [{"name": "subparser_name"}])]} + with self.assertRaises(UserError): + rec.jsonify(bad_subparser, one=True) + + def test_bad_parsers_fail_gracefully(self): + rec = self.category + + logger_patch_path = "odoo.addons.jsonifier.models.models._logger.error" + + # logging is disabled when testing as it's useless and makes build fail. + tools.config["test_enable"] = False + + bad_field_name = ["Name"] + with mock.patch(logger_patch_path) as mocked_logger: + rec.jsonify(bad_field_name, one=True) + mocked_logger.assert_called() + + bad_function_name = {"fields": [{"name": "name", "function": "notafunction"}]} + with mock.patch(logger_patch_path) as mocked_logger: + rec.jsonify(bad_function_name, one=True) + mocked_logger.assert_called() + + bad_subparser = {"fields": [({"name": "name"}, [{"name": "subparser_name"}])]} + with mock.patch(logger_patch_path) as mocked_logger: + rec.jsonify(bad_subparser, one=True) + mocked_logger.assert_called() + + tools.config["test_enable"] = True diff --git a/jsonifier/tests/test_helpers.py b/jsonifier/tests/test_helpers.py new file mode 100644 index 000000000..c7cebf727 --- /dev/null +++ b/jsonifier/tests/test_helpers.py @@ -0,0 +1,45 @@ +# Copyright 2021 Camptocamp SA (https://www.camptocamp.com). +# @author Iván Todorovich +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import SavepointCase + + +class TestJsonifyHelpers(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.partner = cls.env["res.partner"].create( + { + "name": "My Partner", + } + ) + cls.children = cls.env["res.partner"].create( + [ + {"parent_id": cls.partner.id, "name": "Child 1"}, + {"parent_id": cls.partner.id, "name": "Child 2"}, + ] + ) + + def test_helper_m2o_to_id(self): + child = self.children[0] + self.assertEqual( + child._jsonify_m2o_to_id("parent_id"), + child.parent_id.id, + ) + + def test_helper_m2m_to_ids(self): + self.assertEqual( + self.partner._jsonify_x2m_to_ids("child_ids"), + self.partner.child_ids.ids, + ) + + def test_helper_format_duration(self): + # credit_limit is not intended for this, but it's a float field in core + # any float field does the trick here + self.partner.credit_limit = 15.5 + self.assertEqual( + self.partner._jsonify_format_duration("credit_limit"), + "15:30", + ) diff --git a/jsonifier/tests/test_ir_exports_line.py b/jsonifier/tests/test_ir_exports_line.py new file mode 100644 index 000000000..d28501af0 --- /dev/null +++ b/jsonifier/tests/test_ir_exports_line.py @@ -0,0 +1,68 @@ +# Copyright 2017 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.exceptions import ValidationError +from odoo.tests.common import SavepointCase + + +class TestIrExportsLine(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.ir_export = cls.env.ref("jsonifier.ir_exp_partner") + + def test_target_constrains(self): + ir_export_lines_model = self.env["ir.exports.line"] + with self.assertRaises(ValidationError): + # The field into the name must be also into the target + ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "name", + "target": "toto:my_target", + } + ) + with self.assertRaises(ValidationError): + # The hierarchy into the target must be the same as the one into + # the name + ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "child_ids/child_ids/name", + "target": "child_ids:children/name", + } + ) + with self.assertRaises(ValidationError): + # The hierarchy into the target must be the same as the one into + # the name and must contains the same fields as into the name + ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "child_ids/child_ids/name", + "target": "child_ids:children/category_id:category/name", + } + ) + line = ir_export_lines_model.create( + { + "export_id": self.ir_export.id, + "name": "child_ids/child_ids/name", + "target": "child_ids:children/child_ids:children/name", + } + ) + 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, + "instance_method_name": "function_name", + } + ) diff --git a/jsonifier/views/ir_exports_resolver_view.xml b/jsonifier/views/ir_exports_resolver_view.xml new file mode 100644 index 000000000..1f18bcaf0 --- /dev/null +++ b/jsonifier/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/jsonifier/views/ir_exports_view.xml b/jsonifier/views/ir_exports_view.xml new file mode 100644 index 000000000..ddfb5a152 --- /dev/null +++ b/jsonifier/views/ir_exports_view.xml @@ -0,0 +1,38 @@ + + + + ir.exports + 50 + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Export Fields + ir.exports + tree,form + + +
From df7a89a62a9371e04b570dd8972dfe64ff4e8dc6 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Sat, 23 Apr 2022 09:09:23 +0000 Subject: [PATCH 04/20] [UPD] Update jsonifier.pot --- jsonifier/i18n/ca.po | 11 +- jsonifier/i18n/jsonifier.pot | 235 +++++++++++++++++++++++++++++++++++ jsonifier/i18n/zh_CN.po | 7 ++ 3 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 jsonifier/i18n/jsonifier.pot diff --git a/jsonifier/i18n/ca.po b/jsonifier/i18n/ca.po index f7c8cd680..665864179 100644 --- a/jsonifier/i18n/ca.po +++ b/jsonifier/i18n/ca.po @@ -199,6 +199,13 @@ msgstr "" msgid "Python Code" msgstr "" +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + #. module: jsonifier #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target msgid "Target" @@ -207,8 +214,8 @@ msgstr "" #. module: jsonifier #: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target msgid "" -"The complete path to the field where you can specify a target on the step as" -" field:target" +"The complete path to the field where you can specify a target on the step as " +"field:target" msgstr "" #. module: jsonifier diff --git a/jsonifier/i18n/jsonifier.pot b/jsonifier/i18n/jsonifier.pot new file mode 100644 index 000000000..34c42d3fb --- /dev/null +++ b/jsonifier/i18n/jsonifier.pot @@ -0,0 +1,235 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * jsonifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__instance_method_name +msgid "A method defined on the model that takes a record and a field_name" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__active +msgid "Active" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_base +msgid "Base" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_resolver__python_code +msgid "" +"Compute the result from 'value' by setting the variable 'result'.\n" +"For fields resolvers:\n" +":param name: name of the field\n" +":param value: value of the field\n" +":param field_type: type of the field\n" +"For global resolvers:\n" +":param value: JSON dict\n" +":param record: the record" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Configuration" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_uid +msgid "Created by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__create_date +msgid "Created on" +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_resolver_view +#: model:ir.ui.menu,name:jsonifier.ui_exports_resolvers +msgid "Custom Export Resolvers" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__global_resolver_id +msgid "Custom global resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__resolver_id +msgid "Custom resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__display_name +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__display_name +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name +msgid "Display Name" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "Either set a function or a resolver, not both." +msgstr "" + +#. module: jsonifier +#: model:ir.actions.act_window,name:jsonifier.act_ui_exports_view +#: model:ir.ui.menu,name:jsonifier.ui_exports +msgid "Export Fields" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_resolver +msgid "Export Resolver" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports +msgid "Exports" +msgstr "" + +#. module: jsonifier +#: model:ir.model,name:jsonifier.model_ir_exports_line +msgid "Exports Line" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__field +msgid "Field" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__instance_method_name +msgid "Function" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields.selection,name:jsonifier.selection__ir_exports_resolver__type__global +msgid "Global" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__id +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__id +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id +msgid "ID" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__lang_id +msgid "If set, the language in which the field is exported" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__global_resolver_id +msgid "If set, will apply the global resolver to the result" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__resolver_id +msgid "If set, will apply the resolver on the field value" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports__language_agnostic +msgid "" +"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" +msgstr "" + +#. module: jsonifier +#: model_terms:ir.ui.view,arch_db:jsonifier.view_ir_exports +msgid "Index" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__lang_id +msgid "Language" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__language_agnostic +msgid "Language Agnostic" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports____last_update +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line____last_update +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver____last_update +msgid "Last Modified on" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__write_date +msgid "Last Updated on" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__name +msgid "Name" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "Name and Target must have the same hierarchy depth" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__python_code +msgid "Python Code" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target +msgid "Target" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,help:jsonifier.field_ir_exports_line__target +msgid "" +"The complete path to the field where you can specify a target on the step as" +" field:target" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/ir_exports_line.py:0 +#, python-format +msgid "The target must reference the same field as in name '%s' not in '%s'" +msgstr "" + +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type +msgid "Type" +msgstr "" + +#. module: jsonifier +#: code:addons/jsonifier/models/models.py:0 +#, python-format +msgid "Wrong parser configuration for field: `%s`" +msgstr "" diff --git a/jsonifier/i18n/zh_CN.po b/jsonifier/i18n/zh_CN.po index 407cdaf86..1c9955075 100644 --- a/jsonifier/i18n/zh_CN.po +++ b/jsonifier/i18n/zh_CN.po @@ -201,6 +201,13 @@ msgstr "名称和别名必须具有相同的层次结构深度" msgid "Python Code" msgstr "" +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + #. module: jsonifier #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target msgid "Target" From a2cb25ca4930eb65e04f7955bdc2389c4cd9d895 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sat, 23 Apr 2022 09:14:59 +0000 Subject: [PATCH 05/20] [UPD] README.rst --- jsonifier/README.rst | 246 +++++++++++++++++++++++- jsonifier/static/description/index.html | 11 +- 2 files changed, 253 insertions(+), 4 deletions(-) diff --git a/jsonifier/README.rst b/jsonifier/README.rst index 89bcd6c21..c0363cd11 100644 --- a/jsonifier/README.rst +++ b/jsonifier/README.rst @@ -1 +1,245 @@ -wait for the bot ;) +========= +JSONifier +========= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/14.0/jsonifier + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-14-0/server-tools-14-0-jsonifier + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/149/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +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 a simple parser: + + +.. code-block:: python + + parser = [ + 'name', + 'number', + 'create_date', + ('partner_id', ['id', 'display_name', 'ref']) + ('line_id', ['id', ('product_id', ['name']), 'price_unit']) + ] + +In order to be consistent with the Odoo API the jsonify method always +returns a list of objects 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: + +.. code-block:: python + + parser = [ + 'field_name:json_key' + ] + +.. code-block:: python + + + parser = [ + 'name', + 'number', + 'create_date:creationDate', + ('partner_id:partners', ['id', 'display_name', 'ref']) + ('line_id:lines', ['id', ('product_id', ['name']), 'price_unit']) + ] + +If you need to parse the value of a field in a custom way, +you can pass a callable or the name of a method on the model: + +.. code-block:: python + + parser = [ + ('name', "jsonify_name") # method name + ('number', lambda rec, field_name: rec[field_name] * 2)) # callable + ] + +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 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. + +To use these features, a full parser follows the following structure: + +.. code-block:: python + + parser = { + "resolver": 3, + "language_agnostic": True, + "langs": { + False: [ + {'name': 'description'}, + {'name': 'number', 'resolver': 5}, + ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'display_name'}]) + ], + 'fr_FR': [ + {'name': 'description', 'target': 'descriptions_fr'}, + ({'name': 'partner_id', 'target': 'partner'}, [{'name': 'description', 'target': 'description_fr'}]) + ], + } + } + + +One would get 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: + +.. code-block:: python + + parser = { + "resolver": 3, + "fields": [ + {'name': 'description'}, + {'name': 'number', 'resolver': 5}, + ({'name': 'partner_id', 'target': 'partners'}, [{'name': 'display_name'}]), + ], + } + + +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: + +.. code-block:: python + + 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 have a marshaller by ending the target with '=list': +in that case the result is put into a list. + +.. code-block:: python + + parser = { + fields: [ + {'name': 'name'}, + {'name': 'field_1', 'target': 'customTags=list'}, + {'name': 'field_2', 'target': 'customTags=list'}, + ] + } + + +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. + + +NOTE: this module was named `base_jsonify` till version 14.0.1.5.0. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion +* ACSONE +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* BEAU Sébastien +* Raphaël Reverdy +* Laurent Mignon +* Nans Lefebvre +* Simone Orsi +* Iván Todorovich + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/jsonifier/static/description/index.html b/jsonifier/static/description/index.html index a48dbab00..b3f2ecf65 100644 --- a/jsonifier/static/description/index.html +++ b/jsonifier/static/description/index.html @@ -4,7 +4,7 @@ -Base JSONify +JSONifier -
-

Base JSONify

+
+

JSONifier

-

Beta License: LGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

+

Beta License: LGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

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.

@@ -515,7 +515,7 @@ this is in contrast with full parsers which are designed to be language agnostic

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -546,7 +546,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

-

This module is part of the OCA/server-tools project on GitHub.

+

This module is part of the OCA/server-tools project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

From 41b5ac89d0c14b5f48fe5cfba50be2328a0b01b3 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 27 Feb 2023 12:11:05 +0000 Subject: [PATCH 13/20] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: server-tools-16.0/server-tools-16.0-jsonifier Translate-URL: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-jsonifier/ --- jsonifier/i18n/ca.po | 21 +++++++-------------- jsonifier/i18n/zh_CN.po | 27 ++++++++++++--------------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/jsonifier/i18n/ca.po b/jsonifier/i18n/ca.po index 665864179..c162a6937 100644 --- a/jsonifier/i18n/ca.po +++ b/jsonifier/i18n/ca.po @@ -74,13 +74,12 @@ msgid "Custom resolver" msgstr "" #. module: jsonifier -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__display_name -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__display_name #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name msgid "Display Name" msgstr "" #. module: jsonifier +#. odoo-python #: code:addons/jsonifier/models/ir_exports_line.py:0 #, python-format msgid "Either set a function or a resolver, not both." @@ -123,8 +122,6 @@ msgid "Global" msgstr "" #. module: jsonifier -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__id -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__id #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id msgid "ID" msgstr "" @@ -167,8 +164,6 @@ msgid "Language Agnostic" msgstr "" #. module: jsonifier -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports____last_update -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line____last_update #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver____last_update msgid "Last Modified on" msgstr "" @@ -189,6 +184,7 @@ msgid "Name" msgstr "" #. module: jsonifier +#. odoo-python #: code:addons/jsonifier/models/ir_exports_line.py:0 #, python-format msgid "Name and Target must have the same hierarchy depth" @@ -199,13 +195,6 @@ msgstr "" msgid "Python Code" msgstr "" -#. module: jsonifier -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search -msgid "Smart Search" -msgstr "" - #. module: jsonifier #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target msgid "Target" @@ -219,9 +208,12 @@ msgid "" msgstr "" #. module: jsonifier +#. odoo-python #: code:addons/jsonifier/models/ir_exports_line.py:0 #, python-format -msgid "The target must reference the same field as in name '%s' not in '%s'" +msgid "" +"The target must reference the same field as in name '%(name)s' not in " +"'%(name_with_target)s'" msgstr "" #. module: jsonifier @@ -230,6 +222,7 @@ msgid "Type" msgstr "" #. module: jsonifier +#. odoo-python #: code:addons/jsonifier/models/models.py:0 #, python-format msgid "Wrong parser configuration for field: `%s`" diff --git a/jsonifier/i18n/zh_CN.po b/jsonifier/i18n/zh_CN.po index af4aec70b..5474e885a 100644 --- a/jsonifier/i18n/zh_CN.po +++ b/jsonifier/i18n/zh_CN.po @@ -76,13 +76,12 @@ msgid "Custom resolver" msgstr "" #. module: jsonifier -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__display_name -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__display_name #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__display_name msgid "Display Name" msgstr "" #. module: jsonifier +#. odoo-python #: code:addons/jsonifier/models/ir_exports_line.py:0 #, python-format msgid "Either set a function or a resolver, not both." @@ -125,8 +124,6 @@ msgid "Global" msgstr "" #. module: jsonifier -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__id -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__id #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__id msgid "ID" msgstr "" @@ -169,8 +166,6 @@ msgid "Language Agnostic" msgstr "" #. module: jsonifier -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports____last_update -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line____last_update #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver____last_update msgid "Last Modified on" msgstr "" @@ -191,6 +186,7 @@ msgid "Name" msgstr "" #. module: jsonifier +#. odoo-python #: code:addons/jsonifier/models/ir_exports_line.py:0 #, python-format msgid "Name and Target must have the same hierarchy depth" @@ -201,13 +197,6 @@ msgstr "名称和别名必须具有相同的层次结构深度" msgid "Python Code" msgstr "" -#. module: jsonifier -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search -#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search -msgid "Smart Search" -msgstr "" - #. module: jsonifier #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target msgid "Target" @@ -221,10 +210,13 @@ msgid "" msgstr "字段的完整路径,您可以在其中指定步骤作为字段的别名:别名" #. module: jsonifier +#. odoo-python #: code:addons/jsonifier/models/ir_exports_line.py:0 #, python-format -msgid "The target must reference the same field as in name '%s' not in '%s'" -msgstr "别名必须引用与名称相同的字段'%s'不在'%s'" +msgid "" +"The target must reference the same field as in name '%(name)s' not in " +"'%(name_with_target)s'" +msgstr "" #. module: jsonifier #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__type @@ -232,7 +224,12 @@ msgid "Type" msgstr "" #. module: jsonifier +#. odoo-python #: code:addons/jsonifier/models/models.py:0 #, fuzzy, python-format msgid "Wrong parser configuration for field: `%s`" msgstr "错误的解析器配置 %s" + +#, python-format +#~ msgid "The target must reference the same field as in name '%s' not in '%s'" +#~ msgstr "别名必须引用与名称相同的字段'%s'不在'%s'" From 7b2ce3ead225a35cfef91c9c5d6ef617fa7ac91b Mon Sep 17 00:00:00 2001 From: oca-ci Date: Wed, 7 Jun 2023 15:42:30 +0000 Subject: [PATCH 14/20] [UPD] Update jsonifier.pot --- jsonifier/i18n/jsonifier.pot | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/jsonifier/i18n/jsonifier.pot b/jsonifier/i18n/jsonifier.pot index 6153137a8..5e0e442a8 100644 --- a/jsonifier/i18n/jsonifier.pot +++ b/jsonifier/i18n/jsonifier.pot @@ -194,6 +194,13 @@ msgstr "" msgid "Python Code" msgstr "" +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + #. module: jsonifier #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target msgid "Target" From 739f68b03335fadbe53cb4d1316e8386966bd1d1 Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 7 Jun 2023 15:49:40 +0000 Subject: [PATCH 15/20] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: server-tools-16.0/server-tools-16.0-jsonifier Translate-URL: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-jsonifier/ --- jsonifier/i18n/ca.po | 7 +++++++ jsonifier/i18n/zh_CN.po | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/jsonifier/i18n/ca.po b/jsonifier/i18n/ca.po index c162a6937..98c5be20e 100644 --- a/jsonifier/i18n/ca.po +++ b/jsonifier/i18n/ca.po @@ -195,6 +195,13 @@ msgstr "" msgid "Python Code" msgstr "" +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + #. module: jsonifier #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target msgid "Target" diff --git a/jsonifier/i18n/zh_CN.po b/jsonifier/i18n/zh_CN.po index 5474e885a..d2eddda9a 100644 --- a/jsonifier/i18n/zh_CN.po +++ b/jsonifier/i18n/zh_CN.po @@ -197,6 +197,13 @@ msgstr "名称和别名必须具有相同的层次结构深度" msgid "Python Code" msgstr "" +#. module: jsonifier +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__smart_search +#: model:ir.model.fields,field_description:jsonifier.field_ir_exports_resolver__smart_search +msgid "Smart Search" +msgstr "" + #. module: jsonifier #: model:ir.model.fields,field_description:jsonifier.field_ir_exports_line__target msgid "Target" From c888c09d99af17210c57d5f3069aeb9ae2bdaf9d Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sun, 3 Sep 2023 16:49:46 +0000 Subject: [PATCH 16/20] [UPD] README.rst --- jsonifier/README.rst | 15 ++- jsonifier/static/description/index.html | 162 ++++++++++++------------ 2 files changed, 91 insertions(+), 86 deletions(-) diff --git a/jsonifier/README.rst b/jsonifier/README.rst index 365a6969c..6d6e09177 100644 --- a/jsonifier/README.rst +++ b/jsonifier/README.rst @@ -2,10 +2,13 @@ JSONifier ========= -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c2bfb7648f5bdbd7dfc39ce9e3a2169fa7ba35f0d5467e289da141c53a7deded + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -19,11 +22,11 @@ JSONifier .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png :target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-jsonifier :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/149/16.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This module adds a 'jsonify' method to every model of the ORM. It works on the current recordset and requires a single argument 'parser' @@ -202,7 +205,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed +If you spotted it first, help us to smash it by providing a detailed and welcomed `feedback `_. Do not contact contributors directly about support or help with technical issues. diff --git a/jsonifier/static/description/index.html b/jsonifier/static/description/index.html index aaf8a9587..7399fac73 100644 --- a/jsonifier/static/description/index.html +++ b/jsonifier/static/description/index.html @@ -1,20 +1,20 @@ - + - + JSONifier