mirror of
https://github.com/odoo/runbot.git
synced 2025-03-27 13:25:47 +07:00

Until now api routes on runbot have been created through custom website pages in production. We want to unify the API by making a 'public' api, inspired by the way `web_search_read` works. This commit adds: - A route to list all publicly available models - A route to do a read on a public model - A route to fetch the publicly available specification for a model - A public model mixin that provides all the tools required to support the above mentionned routes. The mixin adds the ability to add the `public` attribute on fields. Any field marked as public can then be publicly queried through the controller. Relational fields work in a nested manner (`fields` key in the field's sub-specification) (up to a depth of 10). The public api does not allow going through a relationship back and front (parent->child->parent is NOT allowed). Because we are based on `web_search_read`, we heavily focus on validating the specification, for security reasons, and offset the load of reading to the `web_read` function (we currently don't provide limit metadata).
296 lines
10 KiB
Python
296 lines
10 KiB
Python
from odoo import fields
|
|
from odoo.tests.common import TransactionCase, tagged, new_test_user
|
|
|
|
|
|
@tagged('-at_install', 'post_install')
|
|
class TestSpec(TransactionCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.Parent = cls.env['runbot.test.model.parent']
|
|
cls.Child = cls.env['runbot.test.model.child']
|
|
cls.user_basic = new_test_user(
|
|
cls.env, 'basic',
|
|
)
|
|
|
|
def test_parent_spec(self):
|
|
spec = self.Parent._api_public_specification()
|
|
self.assertNotIn('private_field', spec)
|
|
|
|
self.assertEqual(
|
|
spec['field_bool'],
|
|
{
|
|
'__type': 'boolean',
|
|
}
|
|
)
|
|
|
|
self.assertIn('field_many2one', spec)
|
|
sub_spec = spec['field_many2one']['fields']
|
|
self.assertIn('field_many2many', spec)
|
|
self.assertIn('field_many2many', sub_spec)
|
|
|
|
self.assertTrue(self.Parent._api_verify_specification(spec))
|
|
|
|
def test_child_spec(self):
|
|
spec = self.Child._api_public_specification()
|
|
|
|
self.assertIn('parent_id', spec)
|
|
self.assertIn('data', spec)
|
|
|
|
sub_spec = spec['parent_id']
|
|
# The reverse relation should not be part of the sub spec, as we already
|
|
# traversed that relation
|
|
self.assertNotIn('field_one2many', sub_spec)
|
|
|
|
self.assertTrue(self.Child._api_verify_specification(spec))
|
|
|
|
def test_parent_read_basic(self):
|
|
today = fields.Date.today()
|
|
now = fields.Datetime.now()
|
|
parent = self.Parent.create({
|
|
'field_bool': True,
|
|
'field_date': today,
|
|
'field_datetime': now,
|
|
'field_json': {'test': 1}
|
|
})
|
|
self.assertEqual(
|
|
parent._api_read({'field_bool': {}, 'field_date': {}, 'field_datetime': {}, 'field_json': {}}),
|
|
[{
|
|
'id': parent.id,
|
|
'field_bool': True,
|
|
'field_date': today,
|
|
'field_datetime': now,
|
|
'field_json': {'test': 1}
|
|
}]
|
|
)
|
|
|
|
def test_parent_read_m2o(self):
|
|
parent_1 = self.Parent.create({'field_integer': 1})
|
|
parent_2 = self.Parent.create({'field_integer': 2, 'field_many2one': parent_1.id})
|
|
|
|
# Read without sub spec
|
|
self.assertEqual(
|
|
parent_1._api_read({'field_many2one': {}}),
|
|
[{'id': parent_1.id, 'field_many2one': False}]
|
|
)
|
|
self.assertEqual(
|
|
parent_2._api_read({'field_many2one': {}}),
|
|
[{'id': parent_2.id, 'field_many2one': parent_1.id}]
|
|
)
|
|
# Read with sub spec
|
|
self.assertEqual(
|
|
parent_1._api_read({'field_many2one': {'fields': {'field_integer': {}}}}),
|
|
[{'id': parent_1.id, 'field_many2one': False}]
|
|
)
|
|
self.assertEqual(
|
|
parent_2._api_read({'field_many2one': {'fields': {'field_integer': {}}}}),
|
|
[{'id': parent_2.id, 'field_many2one': {'id': parent_1.id, 'field_integer': parent_1.field_integer}}]
|
|
)
|
|
both = parent_1 | parent_2
|
|
# Read both with sub spec
|
|
self.assertEqual(
|
|
both._api_read({'field_integer': {}, 'field_many2one': {'fields': {'field_integer': {}}}}),
|
|
[
|
|
{'id': parent_1.id, 'field_integer': parent_1.field_integer, 'field_many2one': False},
|
|
{'id': parent_2.id, 'field_integer': parent_2.field_integer, 'field_many2one': {
|
|
'id': parent_1.id, 'field_integer': parent_1.field_integer,
|
|
}},
|
|
]
|
|
)
|
|
|
|
def test_parent_read_one2many(self):
|
|
parent = self.Parent.create({
|
|
'field_float': 13.37,
|
|
'field_one2many': [
|
|
(0, 0, {'data': 1}),
|
|
(0, 0, {'data': 2}),
|
|
(0, 0, {'data': 3}),
|
|
]
|
|
})
|
|
parent_no_o2m = self.Parent.create({
|
|
'field_float': 13.37,
|
|
})
|
|
|
|
# Basic read
|
|
self.assertEqual(
|
|
parent._api_read({
|
|
'field_float': {},
|
|
'field_one2many': {
|
|
'fields': {
|
|
'data': {}
|
|
},
|
|
}
|
|
}),
|
|
[{
|
|
'id': parent.id,
|
|
'field_float': 13.37,
|
|
'field_one2many': [
|
|
{'id': parent.field_one2many[0].id, 'data': 1},
|
|
{'id': parent.field_one2many[1].id, 'data': 2},
|
|
{'id': parent.field_one2many[2].id, 'data': 3},
|
|
]
|
|
}]
|
|
)
|
|
self.assertEqual(
|
|
parent_no_o2m._api_read({
|
|
'field_float': {},
|
|
'field_one2many': {
|
|
'fields': {
|
|
'data': {},
|
|
}
|
|
},
|
|
}),
|
|
[{
|
|
'id': parent_no_o2m.id,
|
|
'field_float': 13.37,
|
|
'field_one2many': [],
|
|
}]
|
|
)
|
|
|
|
# Reading parent_id through field_one2many_computed is allowed since relationship is not known
|
|
self.env.invalidate_all()
|
|
spec = {
|
|
'field_float': {},
|
|
'field_one2many': {
|
|
'fields': {
|
|
'data': {},
|
|
'parent_id': {
|
|
'fields': {
|
|
'field_float': {},
|
|
}
|
|
},
|
|
}
|
|
},
|
|
}
|
|
self.assertEqual(
|
|
parent._api_read(spec),
|
|
[{
|
|
'id': parent.id,
|
|
'field_float': 13.37,
|
|
'field_one2many': [
|
|
{'id': parent.field_one2many[0].id, 'data': 1, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
|
|
{'id': parent.field_one2many[1].id, 'data': 2, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
|
|
{'id': parent.field_one2many[2].id, 'data': 3, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
|
|
]
|
|
}]
|
|
)
|
|
# It should not work with basic user
|
|
self.assertFalse(parent.with_user(self.user_basic)._api_verify_specification(spec))
|
|
# It should however work with the computed version
|
|
spec['field_one2many_computed'] = spec['field_one2many']
|
|
spec.pop('field_one2many')
|
|
self.assertEqual(
|
|
parent.with_user(self.user_basic)._api_read(spec),
|
|
[{
|
|
'id': parent.id,
|
|
'field_float': 13.37,
|
|
'field_one2many_computed': [
|
|
{'id': parent.field_one2many[0].id, 'data': 1, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
|
|
{'id': parent.field_one2many[1].id, 'data': 2, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
|
|
{'id': parent.field_one2many[2].id, 'data': 3, 'parent_id': {'id': parent.id, 'field_float': 13.37}},
|
|
]
|
|
}]
|
|
)
|
|
|
|
def test_parent_read_one2many_private(self):
|
|
# Check the private one2many (field is public, model is not), we can read id but nothing else
|
|
parent = self.Parent.create({
|
|
'field_float': 13.37,
|
|
'field_one2many_private': [
|
|
(0, 0, {'data': 1}),
|
|
]
|
|
}).with_user(self.user_basic)
|
|
spec = parent._api_public_specification()
|
|
self.assertNotIn('fields', spec['field_one2many_private'])
|
|
result = parent._api_read(spec)
|
|
self.assertEqual(
|
|
result[0]['field_one2many_private'],
|
|
[parent.field_one2many_private.id],
|
|
)
|
|
with self.assertRaises(ValueError):
|
|
parent._api_verify_specification({
|
|
'field_one2many_private': {
|
|
'fields': {
|
|
'data': {}
|
|
}
|
|
}
|
|
})
|
|
|
|
|
|
def test_parent_read_m2m(self):
|
|
parent = self.Parent.create({
|
|
'field_integer': 1,
|
|
'field_many2many': [
|
|
(0, 0, {'field_integer': 2}),
|
|
],
|
|
})
|
|
other_parent = parent.field_many2many
|
|
self.assertEqual(
|
|
parent._api_read({'field_integer': {}, 'field_many2many': {}}),
|
|
[
|
|
{'id': parent.id, 'field_many2many': list(parent.field_many2many.ids), 'field_integer': 1},
|
|
],
|
|
)
|
|
self.env.invalidate_all()
|
|
spec = {
|
|
'field_many2many_computed': {
|
|
'fields': {
|
|
'field_many2many_computed': {
|
|
'fields': {
|
|
'field_integer': {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.assertEqual(
|
|
parent._api_read(spec),
|
|
[
|
|
{'id': parent.id, 'field_many2many_computed': [
|
|
{'id': other_parent.id, 'field_many2many_computed': [
|
|
{'id': parent.id, 'field_integer': 1}
|
|
]}
|
|
]}
|
|
]
|
|
)
|
|
self.assertFalse(parent.with_user(self.user_basic)._api_verify_specification(spec))
|
|
|
|
def test_parent_read_cyclic_parent(self):
|
|
parent_1 = self.Parent.create({
|
|
'field_integer': 1,
|
|
})
|
|
parent_2 = self.Parent.create({
|
|
'field_integer': 2,
|
|
'field_many2one': parent_1.id,
|
|
})
|
|
parent_1.field_many2one = parent_2
|
|
self.env.invalidate_all()
|
|
both = (parent_1 | parent_2)
|
|
self.assertEqual(
|
|
both._api_read({'field_many2one': {}}),
|
|
[
|
|
{'id': parent_1.id, 'field_many2one': parent_2.id},
|
|
{'id': parent_2.id, 'field_many2one': parent_1.id},
|
|
]
|
|
)
|
|
self.assertEqual(
|
|
both.with_user(self.user_basic)._api_read({'field_many2one': {}}),
|
|
[
|
|
{'id': parent_1.id, 'field_many2one': parent_2.id},
|
|
{'id': parent_2.id, 'field_many2one': parent_1.id},
|
|
]
|
|
)
|
|
spec = {'field_many2one': {'fields': {'field_many2one': {}}}}
|
|
self.assertEqual(
|
|
both._api_read(spec),
|
|
[
|
|
{'id': parent_1.id, 'field_many2one': {'id': parent_2.id, 'field_many2one': parent_1.id}},
|
|
{'id': parent_2.id, 'field_many2one': {'id': parent_1.id, 'field_many2one': parent_2.id}},
|
|
]
|
|
)
|
|
self.assertFalse(both.with_user(self.user_basic)._api_verify_specification(spec))
|
|
|
|
|