[TEST] base_jsonify: test and document code

pull/2418/head
nans 2020-10-11 18:57:52 +02:00 committed by Sébastien BEAU
parent a648e21313
commit 3812375799
6 changed files with 128 additions and 34 deletions

View File

@ -96,16 +96,31 @@ To use these features, a full parser follows the following structure:
False: [
{'name': 'description'},
{'name': 'number', 'resolver': 5},
({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'display_name'}])
({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'display_name'}])
],
'fr_FR': [
{'name': 'description', 'alias': 'descriptions_fr'},
({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'description', 'alias': 'description_fr'}])
({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'description', 'alias': 'description_fr'}])
],
}
}
One would get the a result having this structure (note that the translated fields are merged in the same dictionary):
.. code-block:: python
exported_json == {
"description": "English description",
"description_fr": "French description, voilà",
"number": 42,
"partner": {
"display_name": "partner name",
"description_fr": "French description of that partner",
},
}
Note that a resolver can be passed either as a recordset or as an id, so as to be fully serializable.
A slightly simpler version in case the translation of fields is not needed,
but other features like custom resolvers are:
@ -114,7 +129,6 @@ but other features like custom resolvers are:
parser = {
"resolver": 3,
"language_agnostic": True,
"fields": [
{'name': 'description'},
{'name': 'number', 'resolver': 5},
@ -123,6 +137,15 @@ but other features like custom resolvers are:
}
By passing the `fields` key instead of `langs`, we have essentially the same behaviour as simple parsers,
with the added benefit of being able to use resolvers.
Standard use-cases of resolvers are:
- give field-specific defaults (e.g. `""` instead of `None`)
- cast a field type (e.g. `int()`)
- alias a particular field for a specific export
- ...
A simple parser is simply translated into a full parser at export.
If the global resolver is given, then the json_dict goes through:
@ -135,19 +158,17 @@ Which allows to add external data from the context or transform the dictionary
if necessary. Similarly if given for a field the resolver evaluates the result.
It is possible for an alias to end with a '*':
in that case the result it put into a list.
in that case the result is put into a list.
.. code-block:: python
parser = {
"langs": {
False: [
fields: [
{'name': 'name'},
{'name': 'field_1', 'alias': 'customTags*'},
{'name': 'field_2', 'alias': 'customTags*'},
]
}
}
Would result in the following json structure:

View File

@ -7,7 +7,13 @@ from collections import OrderedDict
from odoo import fields, models
def partition(l, accessor): # -> Dict
def partition(l, accessor):
"""Partition an iterable (e.g. a recordset) according to an accessor (e.g. a lambda)
Return a dictionary whose keys are the values obtained from accessor, and values
are the items that have this value.
Example: partition([{"name": "ax"}, {"name": "by"}], lambda x: "x" in x["name"])
=> {True: [{"name": "ax"}], False: [{"name": "by"}]}
"""
result = {}
for item in l:
key = accessor(item)

View File

@ -69,16 +69,31 @@ To use these features, a full parser follows the following structure:
False: [
{'name': 'description'},
{'name': 'number', 'resolver': 5},
({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'display_name'}])
({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'display_name'}])
],
'fr_FR': [
{'name': 'description', 'alias': 'descriptions_fr'},
({'name': 'partner_id', 'alias': 'partners'}, [{'name': 'description', 'alias': 'description_fr'}])
({'name': 'partner_id', 'alias': 'partner'}, [{'name': 'description', 'alias': 'description_fr'}])
],
}
}
One would get the a result having this structure (note that the translated fields are merged in the same dictionary):
.. code-block:: python
exported_json == {
"description": "English description",
"description_fr": "French description, voilà",
"number": 42,
"partner": {
"display_name": "partner name",
"description_fr": "French description of that partner",
},
}
Note that a resolver can be passed either as a recordset or as an id, so as to be fully serializable.
A slightly simpler version in case the translation of fields is not needed,
but other features like custom resolvers are:
@ -87,7 +102,6 @@ but other features like custom resolvers are:
parser = {
"resolver": 3,
"language_agnostic": True,
"fields": [
{'name': 'description'},
{'name': 'number', 'resolver': 5},
@ -96,6 +110,15 @@ but other features like custom resolvers are:
}
By passing the `fields` key instead of `langs`, we have essentially the same behaviour as simple parsers,
with the added benefit of being able to use resolvers.
Standard use-cases of resolvers are:
- give field-specific defaults (e.g. `""` instead of `None`)
- cast a field type (e.g. `int()`)
- alias a particular field for a specific export
- ...
A simple parser is simply translated into a full parser at export.
If the global resolver is given, then the json_dict goes through:
@ -108,19 +131,17 @@ Which allows to add external data from the context or transform the dictionary
if necessary. Similarly if given for a field the resolver evaluates the result.
It is possible for an alias to end with a '*':
in that case the result it put into a list.
in that case the result is put into a list.
.. code-block:: python
parser = {
"langs": {
False: [
fields: [
{'name': 'name'},
{'name': 'field_1', 'alias': 'customTags*'},
{'name': 'field_2', 'alias': 'customTags*'},
]
}
}
Would result in the following json structure:

View File

@ -424,22 +424,33 @@ It is also to specify a lang to extract the translation of any given field.</p>
<span class="kc">False</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'number'</span><span class="p">,</span> <span class="s1">'resolver'</span><span class="p">:</span> <span class="mi">5</span><span class="p">},</span>
<span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'partners'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'display_name'</span><span class="p">}])</span>
<span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'partner'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'display_name'</span><span class="p">}])</span>
<span class="p">],</span>
<span class="s1">'fr_FR'</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'descriptions_fr'</span><span class="p">},</span>
<span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'partners'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'description_fr'</span><span class="p">}])</span>
<span class="p">({</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'partner_id'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'partner'</span><span class="p">},</span> <span class="p">[{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'description_fr'</span><span class="p">}])</span>
<span class="p">],</span>
<span class="p">}</span>
<span class="p">}</span>
</pre>
<p>One would get the a result having this structure (note that the translated fields are merged in the same dictionary):</p>
<pre class="code python literal-block">
<span class="n">exported_json</span> <span class="o">==</span> <span class="p">{</span>
<span class="s2">&quot;description&quot;</span><span class="p">:</span> <span class="s2">&quot;English description&quot;</span><span class="p">,</span>
<span class="s2">&quot;description_fr&quot;</span><span class="p">:</span> <span class="s2">&quot;French description, voilà&quot;</span><span class="p">,</span>
<span class="s2">&quot;number&quot;</span><span class="p">:</span> <span class="mi">42</span><span class="p">,</span>
<span class="s2">&quot;partner&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">&quot;display_name&quot;</span><span class="p">:</span> <span class="s2">&quot;partner name&quot;</span><span class="p">,</span>
<span class="s2">&quot;description_fr&quot;</span><span class="p">:</span> <span class="s2">&quot;French description of that partner&quot;</span><span class="p">,</span>
<span class="p">},</span>
<span class="p">}</span>
</pre>
<p>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:</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">&quot;resolver&quot;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="s2">&quot;language_agnostic&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
<span class="s2">&quot;fields&quot;</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'description'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'number'</span><span class="p">,</span> <span class="s1">'resolver'</span><span class="p">:</span> <span class="mi">5</span><span class="p">},</span>
@ -447,6 +458,13 @@ but other features like custom resolvers are:</p>
<span class="p">],</span>
<span class="p">}</span>
</pre>
<p>By passing the <cite>fields</cite> key instead of <cite>langs</cite>, we have essentially the same behaviour as simple parsers,
with the added benefit of being able to use resolvers.</p>
<p>Standard use-cases of resolvers are:
- give field-specific defaults (e.g. <cite>“”</cite> instead of <cite>None</cite>)
- cast a field type (e.g. <cite>int()</cite>)
- alias a particular field for a specific export
- …</p>
<p>A simple parser is simply translated into a full parser at export.</p>
<p>If the global resolver is given, then the json_dict goes through:</p>
<pre class="code python literal-block">
@ -455,16 +473,14 @@ but other features like custom resolvers are:</p>
<p>Which allows to add external data from the context or transform the dictionary
if necessary. Similarly if given for a field the resolver evaluates the result.</p>
<p>It is possible for an alias to end with a *:
in that case the result it put into a list.</p>
in that case the result is put into a list.</p>
<pre class="code python literal-block">
<span class="n">parser</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">&quot;langs&quot;</span><span class="p">:</span> <span class="p">{</span>
<span class="kc">False</span><span class="p">:</span> <span class="p">[</span>
<span class="n">fields</span><span class="p">:</span> <span class="p">[</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'name'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'field_1'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'customTags*'</span><span class="p">},</span>
<span class="p">{</span><span class="s1">'name'</span><span class="p">:</span> <span class="s1">'field_2'</span><span class="p">,</span> <span class="s1">'alias'</span><span class="p">:</span> <span class="s1">'customTags*'</span><span class="p">},</span>
<span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>
</pre>
<p>Would result in the following json structure:</p>

View File

@ -2,6 +2,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields
from odoo.exceptions import UserError
from odoo.tests.common import SavepointCase
@ -281,3 +282,16 @@ class TestParser(SavepointCase):
json = self.category.jsonify(export.get_json_parser())[0]
self.assertEqual(json, {"name": "yeah!"})
def test_bad_parsers(self):
bad_field_name = ["Name"]
with self.assertRaises(KeyError):
self.category.jsonify(bad_field_name, one=True)
bad_function_name = {"fields": [{"name": "name", "function": "notafunction"}]}
with self.assertRaises(UserError):
self.category.jsonify(bad_function_name, one=True)
bad_subparser = {"fields": [({"name": "name"}, [{"name": "subparser_name"}])]}
with self.assertRaises(UserError):
self.category.jsonify(bad_subparser, one=True)

View File

@ -49,3 +49,19 @@ class TestIrExportsLine(TransactionCase):
}
)
self.assertTrue(line)
def test_resolver_function_constrains(self):
resolver = self.env["ir.exports.resolver"].create(
{"python_code": "result = value", "type": "field"}
)
ir_export_lines_model = self.env["ir.exports.line"]
with self.assertRaises(ValidationError):
# the callable should be an existing model function, but it's not checked
ir_export_lines_model.create(
{
"export_id": self.ir_export.id,
"name": "name",
"resolver_id": resolver.id,
"function": "function_name",
}
)