Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion base_rest_datamodel/restapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def to_response(self, service, result):
json = [i.dump() for i in result]
else:
json = result.dump()
errors = ModelClass.validate(json, many=self._is_list)
errors = ModelClass.validate(
json, many=self._is_list, unknown=marshmallow.EXCLUDE
)
if errors:
raise SystemError(_("Invalid Response %s") % errors)
return json
Expand Down
1 change: 1 addition & 0 deletions base_rest_datamodel/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_response
76 changes: 76 additions & 0 deletions base_rest_datamodel/tests/test_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright 2021 Wakari SRL
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import marshmallow
import mock

from odoo.addons.base_rest_datamodel import restapi
from odoo.addons.datamodel import fields
from odoo.addons.datamodel.core import Datamodel
from odoo.addons.datamodel.tests import common


class TestDataModel(common.DatamodelRegistryCase):
def _to_response(self, instance):
restapi_datamodel = restapi.Datamodel(instance._name)
mock_service = mock.Mock()
mock_service.env = self.env
return restapi_datamodel.to_response(mock_service, instance)

def test_to_response(self):
class Datamodel1(Datamodel):
_name = "datamodel1"

name = fields.String(required=True, allow_none=False)

Datamodel1._build_datamodel(self.datamodel_registry)
instance = self.env.datamodels["datamodel1"](name="Instance 1")
res = self._to_response(instance)
self.assertEqual(res["name"], instance.name)

def test_to_response_dump_only(self):
class Datamodel2(Datamodel):
_name = "datamodel2"

name = fields.String(required=True, allow_none=False, dump_only=True)

Datamodel2._build_datamodel(self.datamodel_registry)
schema = self.env.datamodels["datamodel2"].get_schema()
self.assertEqual(schema.unknown, "raise")
msg = r"{'name': \['Unknown field.'\]}"
with self.assertRaisesRegex(marshmallow.exceptions.ValidationError, msg):
# confirmation that "name" cannot be loaded
self.env.datamodels["datamodel2"].load({"name": "Failure"})
instance = self.env.datamodels["datamodel2"](name="Instance 2")
res = self._to_response(instance)
self.assertEqual(res["name"], instance.name)
# schema 'unknown' is back to "raise"
self.assertEqual(schema.unknown, "raise")

def test_to_response_dump_only_nested(self):
class Datamodel3(Datamodel):
_name = "datamodel3"

child = fields.NestedModel("nested_datamodel")

class NestedDatamodel(Datamodel):
_name = "nested_datamodel"

name = fields.String(required=True, allow_none=False, dump_only=True)

NestedDatamodel._build_datamodel(self.datamodel_registry)
Datamodel3._build_datamodel(self.datamodel_registry)
for datamodel_name in ("datamodel3", "nested_datamodel"):
schema = self.env.datamodels[datamodel_name].get_schema()
self.assertEqual(schema.unknown, "raise")
msg = r"{'name': \['Unknown field.'\]}"
with self.assertRaisesRegex(marshmallow.exceptions.ValidationError, msg):
# confirmation that child "name" cannot be loaded
self.env.datamodels["datamodel3"].load({"child": {"name": "Failure"}})
child_instance = NestedDatamodel(name="Child Instance")
instance = self.env.datamodels["datamodel3"](child=child_instance)
res = self._to_response(instance)
self.assertEqual(res["child"]["name"], child_instance.name)
for datamodel_name in ("datamodel3", "nested_datamodel"):
schema = self.env.datamodels[datamodel_name].get_schema()
# schema 'unknown' is back to "raise"
self.assertEqual(schema.unknown, "raise")
2 changes: 1 addition & 1 deletion base_rest_demo/datamodels/partner_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ class PartnerInfo(Datamodel):
phone = fields.String(required=False, allow_none=True)
state = NestedModel("state.info")
country = NestedModel("country.info")
is_componay = fields.Boolean(required=False, allow_none=False)
is_company = fields.Boolean(required=False, allow_none=False)
40 changes: 35 additions & 5 deletions datamodel/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import functools
import logging
from collections import OrderedDict, defaultdict
from contextlib import ExitStack

from marshmallow import INCLUDE

from odoo.api import Environment
from odoo.tools import LastOrderedSet, OrderedSet
Expand Down Expand Up @@ -41,12 +44,20 @@ def _get_addon_name(full_name):
return addon_name


def _get_nested_schemas(schema):
res = [schema]
for field in schema.fields.values():
if getattr(field, "schema", None):
res += _get_nested_schemas(field.schema)
return res


class DatamodelDatabases(dict):
""" Holds a registry of datamodels for each database """


class DatamodelRegistry(object):
""" Store all the datamodel and allow to retrieve them by name
"""Store all the datamodel and allow to retrieve them by name

The key is the ``_name`` of the datamodels.

Expand Down Expand Up @@ -98,7 +109,7 @@ def __make_object__(self, data, **kwargs):


class MetaDatamodel(ModelMeta):
""" Metaclass for Datamodel
"""Metaclass for Datamodel

Every new :class:`Datamodel` will be added to ``_modules_datamodels``,
that will be used by the datamodel builder.
Expand Down Expand Up @@ -131,9 +142,16 @@ def __init__(self, name, bases, attrs):

self._modules_datamodels[self._module].append(self)

def __call__(self, *args, **kwargs):
"""Allow to set any field (including 'dump_only') at instantiation
This is not an issue thanks to cleanup during (de)serialization
"""
kwargs["unknown"] = kwargs.get("unknown", INCLUDE)
return super().__call__(*args, **kwargs)


class Datamodel(MarshmallowModel, metaclass=MetaDatamodel):
""" Main Datamodel Model
"""Main Datamodel Model

All datamodels have a Python inheritance either on
:class:`Datamodel`.
Expand Down Expand Up @@ -207,9 +225,21 @@ def get_schema(cls, **kwargs):
"""
return cls.__get_schema_class__(**kwargs)

@classmethod
def validate(cls, data, context=None, many=None, partial=None, unknown=None):
Comment thread
fdegrave marked this conversation as resolved.
schema = cls.__get_schema_class__(
context=context, partial=partial, unknown=unknown
)
all_schemas = _get_nested_schemas(schema)
with ExitStack() as stack:
Comment thread
fdegrave marked this conversation as resolved.
# propagate 'unknown' to each nested schema during validate
for nested_schema in all_schemas:
stack.enter_context(cls.propagate_unknwown(nested_schema, unknown))
return schema.validate(data, many=many, partial=partial)

@classmethod
def _build_datamodel(cls, registry):
""" Instantiate a given Datamodel in the datamodels registry.
"""Instantiate a given Datamodel in the datamodels registry.

This method is called at the end of the Odoo's registry build. The
caller is :meth:`datamodel.builder.DatamodelBuilder.load_datamodels`.
Expand Down Expand Up @@ -350,7 +380,7 @@ def _build_datamodel(cls, registry):

@classmethod
def _complete_datamodel_build(cls):
""" Complete build of the new datamodel class
"""Complete build of the new datamodel class

After the datamodel has been built from its bases, this method is
called, and can be used to customize the class before it can be used.
Expand Down