From 25a2134df799f860db61e708b46eb738dffcd38a Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Wed, 21 Aug 2019 09:48:52 +0200 Subject: [PATCH 01/37] datamodel: New addon allowing you to define dataclass like models serialisable to/from json/yaml --- datamodel/README.rst | 86 +++++ datamodel/__init__.py | 2 + datamodel/__manifest__.py | 19 ++ datamodel/builder.py | 97 ++++++ datamodel/core.py | 370 +++++++++++++++++++++ datamodel/datamodels/__init__.py | 1 + datamodel/datamodels/base.py | 15 + datamodel/fields.py | 41 +++ datamodel/static/description/icon.png | Bin 0 -> 9455 bytes datamodel/tests/__init__.py | 1 + datamodel/tests/common.py | 221 +++++++++++++ datamodel/tests/test_build_datamodel.py | 407 ++++++++++++++++++++++++ 12 files changed, 1260 insertions(+) create mode 100644 datamodel/README.rst create mode 100644 datamodel/__init__.py create mode 100644 datamodel/__manifest__.py create mode 100644 datamodel/builder.py create mode 100644 datamodel/core.py create mode 100644 datamodel/datamodels/__init__.py create mode 100644 datamodel/datamodels/base.py create mode 100644 datamodel/fields.py create mode 100644 datamodel/static/description/icon.png create mode 100644 datamodel/tests/__init__.py create mode 100644 datamodel/tests/common.py create mode 100644 datamodel/tests/test_build_datamodel.py diff --git a/datamodel/README.rst b/datamodel/README.rst new file mode 100644 index 000000000..1dda13fb9 --- /dev/null +++ b/datamodel/README.rst @@ -0,0 +1,86 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +========= +Datamodel +========= + +This addon allows you to define simple data models supporting serialization/deserialization + +Installation +============ + +To install this module, you need to: + +#. Do this ... + +Configuration +============= + +To configure this module, you need to: + +#. Go to ... + +.. figure:: path/to/local/image.png + :alt: alternative description + :width: 600 px + +Usage +===== + +To use this module, you need to: + +#. Go to ... + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/{repo_id}/{branch} + +.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt +.. branch is "8.0" for example + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Firstname Lastname +* Second Person + +Funders +------- + +The development of this module has been financially supported by: + +* Company 1 name +* Company 2 name + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/datamodel/__init__.py b/datamodel/__init__.py new file mode 100644 index 000000000..76665ca2a --- /dev/null +++ b/datamodel/__init__.py @@ -0,0 +1,2 @@ +from . import builder +from . import datamodels diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py new file mode 100644 index 000000000..762078f1c --- /dev/null +++ b/datamodel/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Datamodel", + "summary": """ + This addon allows you to define simple data models supporting + serialization/deserialization""", + "version": "12.0.1.0.0", + "license": "AGPL-3", + "development_status": "Beta", + "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", + "maintainers": ["lmignon"], + "website": "https://acsone.eu/", + "depends": [], + "data": [], + "demo": [], + "external_dependencies": {"python": ["marshmallow", "marshmallow_objects"]}, +} diff --git a/datamodel/builder.py b/datamodel/builder.py new file mode 100644 index 000000000..c73dcd1fd --- /dev/null +++ b/datamodel/builder.py @@ -0,0 +1,97 @@ +# Copyright 2017 Camptocamp SA +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +""" + +Datamodels Builder +================== + +Build the datamodels at the build of a registry. + +""" +from odoo import api, models, modules + +from .core import DEFAULT_CACHE_SIZE, DatamodelRegistry, _datamodel_databases + + +class DatamodelBuilder(models.AbstractModel): + """ Build the datamodel classes + + And register them in a global registry. + + Every time an Odoo registry is built, the know datamodels are cleared and + rebuilt as well. The Datamodel classes are built using the same mechanism + than Odoo's Models: a final class is created, taking every Datamodels with + a ``_name`` and applying Datamodels with an ``_inherits`` upon them. + + The final Datamodel classes are registered in global registry. + + This class is an Odoo model, allowing us to hook the build of the + datamodels at the end of the Odoo's registry loading, using + ``_register_hook``. This method is called after all modules are loaded, so + we are sure that we have all the datamodels Classes and in the correct + order. + + """ + + _name = "datamodel.builder" + _description = "Datamodel Builder" + + _datamodels_registry_cache_size = DEFAULT_CACHE_SIZE + + @api.model_cr + def _register_hook(self): + # This method is called by Odoo when the registry is built, + # so in case the registry is rebuilt (cache invalidation, ...), + # we have to rebuild the datamodels. We use a new + # registry so we have an empty cache and we'll add datamodels in it. + datamodels_registry = self._init_global_registry() + self.build_registry(datamodels_registry) + datamodels_registry.ready = True + + def _init_global_registry(self): + datamodels_registry = DatamodelRegistry( + cachesize=self._datamodels_registry_cache_size + ) + _datamodel_databases[self.env.cr.dbname] = datamodels_registry + return datamodels_registry + + def build_registry(self, datamodels_registry, states=None, exclude_addons=None): + if not states: + states = ("installed", "to upgrade") + # lookup all the installed (or about to be) addons and generate + # the graph, so we can load the datamodels following the order + # of the addons' dependencies + graph = modules.graph.Graph() + graph.add_module(self.env.cr, "base") + + query = "SELECT name " "FROM ir_module_module " "WHERE state IN %s " + params = [tuple(states)] + if exclude_addons: + query += " AND name NOT IN %s " + params.append(tuple(exclude_addons)) + self.env.cr.execute(query, params) + + module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph] + graph.add_modules(self.env.cr, module_list) + + for module in graph: + self.load_datamodels(module.name, datamodels_registry=datamodels_registry) + + def load_datamodels(self, module, datamodels_registry=None): + """ Build every datamodel known by MetaDatamodel for an odoo module + + The final datamodel (composed by all the Datamodel classes in this + module) will be pushed into the registry. + + :param module: the name of the addon for which we want to load + the datamodels + :type module: str | unicode + :param registry: the registry in which we want to put the Datamodel + :type registry: :py:class:`~.core.DatamodelRegistry` + """ + datamodels_registry = ( + datamodels_registry or _datamodel_databases[self.env.cr.dbname] + ) + datamodels_registry.load_datamodels(module) diff --git a/datamodel/core.py b/datamodel/core.py new file mode 100644 index 000000000..f8f744e27 --- /dev/null +++ b/datamodel/core.py @@ -0,0 +1,370 @@ +# Copyright 2017 Camptocamp SA +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from collections import OrderedDict, defaultdict + +from odoo.api import Environment +from odoo.tools import LastOrderedSet, OrderedSet + +_logger = logging.getLogger(__name__) + +try: + import marshmallow + from marshmallow_objects.models import Model as MarshmallowModel, ModelMeta +except ImportError: + _logger.debug("Cannot import 'marshmallow_objects'.") + + +# The Cache size represents the number of items, so the number +# of datamodels (include abstract datamodels) we will keep in the LRU +# cache. We would need stats to know what is the average but this is a bit +# early. +DEFAULT_CACHE_SIZE = 512 + + +# this is duplicated from odoo.models.MetaModel._get_addon_name() which we +# unfortunately can't use because it's an instance method and should have been +# a @staticmethod +def _get_addon_name(full_name): + # The (Odoo) module name can be in the ``odoo.addons`` namespace + # or not. For instance, module ``sale`` can be imported as + # ``odoo.addons.sale`` (the right way) or ``sale`` (for backward + # compatibility). + module_parts = full_name.split(".") + if len(module_parts) > 2 and module_parts[:2] == ["odoo", "addons"]: + addon_name = module_parts[2] + else: + addon_name = module_parts[0] + return addon_name + + +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 + + The key is the ``_name`` of the datamodels. + + This is an OrderedDict, because we want to keep the registration order of + the datamodels, addons loaded first have their datamodels found first. + + The :attr:`ready` attribute must be set to ``True`` when all the datamodels + are loaded. + + """ + + def __init__(self, cachesize=DEFAULT_CACHE_SIZE): + self._datamodels = OrderedDict() + self._loaded_modules = set() + self.ready = False + + def __getitem__(self, key): + return self._datamodels[key] + + def __setitem__(self, key, value): + self._datamodels[key] = value + + def __contains__(self, key): + return key in self._datamodels + + def get(self, key, default=None): + return self._datamodels.get(key, default) + + def __iter__(self): + return iter(self._datamodels) + + def load_datamodels(self, module): + if module in self._loaded_modules: + return + for datamodel_class in MetaDatamodel._modules_datamodels[module]: + datamodel_class._build_datamodel(self) + self._loaded_modules.add(module) + + +# We will store a DatamodeltRegistry per database here, +# it will be cleared and updated when the odoo's registry is rebuilt +_datamodel_databases = DatamodelDatabases() + + +@marshmallow.post_load +def __make_object__(self, data, **kwargs): + datamodel = self._registry[self._datamodel_name] + return datamodel(__post_load__=True, __schema__=self, **data) + + +class MetaDatamodel(ModelMeta): + """ Metaclass for Datamodel + + Every new :class:`Datamodel` will be added to ``_modules_datamodels``, + that will be used by the datamodel builder. + + """ + + _modules_datamodels = defaultdict(list) + + def __init__(self, name, bases, attrs): + + if not self._register: + self._register = True + super(MetaDatamodel, self).__init__(name, bases, attrs) + + return + + # If datamodels are declared in tests, exclude them from the + # "datamodels of the addon" list. If not, when we use the + # "load_datamodels" method, all the test datamodels would be loaded. + # This should never be an issue when running the app normally, as the + # Python tests should never be executed. But this is an issue when a + # test creates a test datamodels for the purpose of the test, then a + # second tests uses the "load_datamodels" to load all the addons of the + # module: it will load the datamodel of the previous test. + if "tests" in self.__module__.split("."): + return + + if not hasattr(self, "_module"): + self._module = _get_addon_name(self.__module__) + + self._modules_datamodels[self._module].append(self) + + +class Datamodel(MarshmallowModel, metaclass=MetaDatamodel): + """ Main Datamodel Model + + All datamodels have a Python inheritance either on + :class:`Datamodel`. + + Inheritance mechanism + The inheritance mechanism is like the Odoo's one for Models. Each + datamodel has a ``_name``. This is the absolute minimum in a Datamodel + class. + + :: + + from marshmallow import fields + from odoo.addons.datamode.core import Datamodel + + class MyDatamodel(Datamodel): + _name = 'my.datamodel' + + name = fields.String() + + Every datamodel implicitly inherit from the `'base'` datamodel. + + There are two close but distinct inheritance types, which look + familiar if you already know Odoo. The first uses ``_inherit`` with + an existing name, the name of the datamodel we want to extend. With + the following example, ``my.datamodel`` is now able to speak and to + yell. + + :: + + class MyDatamodel(Datamodel): # name of the class does not matter + _inherit = 'my.datamodel' + + + The second has a different ``_name``, it creates a new datamodel, + including the behavior of the inherited datamodel, but without + modifying it. + + :: + + class AnotherDatamodel(Datamodel): + _name = 'another.datamodel' + _inherit = 'my.datamodel' + + age = fields.Int() + """ + + _register = False + _registry = None # DatamodelResgistry initialized by the metaclass + + # used for inheritance + _name = None #: Name of the datamodel + + #: Name or list of names of the datamodel(s) to inherit from + _inherit = None + + def __init__(self, *args, **kwargs): + super(Datamodel, self).__init__(*args, **kwargs) + self.__schema__.context["_registry"] = self.registry + + @property + def registry(self): + """ Current datamodels registry""" + return self._registry + + @classmethod + def load(cls, data, context=None, many=None, partial=None): + ctx = (context or {}).copy() + ctx["_registry"] = cls._registry + return super(Datamodel, cls).load( + data=data, context=ctx, many=many, partial=partial + ) + + @classmethod + def _build_datamodel(cls, 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`. + + It generates new classes, which will be the Datamodel classes we will + be using. The new classes are generated following the inheritance + of ``_inherit``. It ensures that the ``__bases__`` of the generated + Datamodel classes follow the ``_inherit`` chain. + + Once a Datamodel class is created, it adds it in the Datamodel Registry + (:class:`DatamodelRegistry`), so it will be available for + lookups. + + At the end of new class creation, a hook method + :meth:`_complete_datamodel_build` is called, so you can customize + further the created datamodels. + + The following code is roughly the same than the Odoo's one for + building Models. + + """ + + # In the simplest case, the datamodel's registry class inherits from + # cls and the other classes that define the datamodel in a flat + # hierarchy. The registry contains the instance ``datamodel`` (on the + # left). Its class, ``DatamodelClass``, carries inferred metadata that + # is shared between all the datamodel's instances for this registry + # only. + # + # class A1(Datamodel): Datamodel + # _name = 'a' / | \ + # A3 A2 A1 + # class A2(Datamodel): \ | / + # _inherit = 'a' DatamodelClass + # + # class A3(Datamodel): + # _inherit = 'a' + # + # When a datamodel is extended by '_inherit', its base classes are + # modified to include the current class and the other inherited + # datamodel classes. + # Note that we actually inherit from other ``DatamodelClass``, so that + # extensions to an inherited datamodel are immediately visible in the + # current datamodel class, like in the following example: + # + # class A1(Datamodel): + # _name = 'a' Datamodel + # / / \ \ + # class B1(Datamodel): / A2 A1 \ + # _name = 'b' / \ / \ + # B2 DatamodelA B1 + # class B2(Datamodel): \ | / + # _name = 'b' \ | / + # _inherit = ['b', 'a'] \ | / + # DatamodelB + # class A2(Datamodel): + # _inherit = 'a' + + # determine inherited datamodels + parents = cls._inherit + if isinstance(parents, str): + parents = [parents] + elif parents is None: + parents = [] + + if cls._name in registry and not parents: + raise TypeError( + "Datamodel %r (in class %r) already exists. " + "Consider using _inherit instead of _name " + "or using a different _name." % (cls._name, cls) + ) + + # determine the datamodel's name + name = cls._name or (len(parents) == 1 and parents[0]) + + if not name: + raise TypeError("Datamodel %r must have a _name" % cls) + + # all datamodels except 'base' implicitly inherit from 'base' + if name != "base": + parents = list(parents) + ["base"] + + # create or retrieve the datamodel's class + if name in parents: + if name not in registry: + raise TypeError("Datamodel %r does not exist in registry." % name) + + # determine all the classes the datamodel should inherit from + bases = LastOrderedSet([cls]) + for parent in parents: + if parent not in registry: + raise TypeError( + "Datamodel %r inherits from non-existing datamodel %r." + % (name, parent) + ) + parent_class = registry[parent] + if parent == name: + for base in parent_class.__bases__: + bases.add(base) + else: + bases.add(parent_class) + parent_class._inherit_children.add(name) + + if name in parents: + DatamodelClass = registry[name] + # Add the new bases to the existing model since the class into + # the registry could already be used into an inherit + DatamodelClass.__bases__ = tuple(bases) + # We must update the marshmallow schema on the existing datamodel + # class to include those inherited + parent_schemas = [] + for parent in bases: + if issubclass(parent, MarshmallowModel): + parent_schemas.append(parent.__schema_class__) + schema_class = type(name + "Schema", tuple(parent_schemas), {}) + DatamodelClass.__schema_class__ = schema_class + else: + attrs = { + "_name": name, + "_register": False, + # names of children datamodel + "_inherit_children": OrderedSet(), + } + if name == "base": + attrs["_registry"] = registry + DatamodelClass = type(name, tuple(bases), attrs) + + setattr(DatamodelClass.__schema_class__, "_registry", registry) # noqa: B010 + setattr(DatamodelClass.__schema_class__, "_datamodel_name", name) # noqa: B010 + setattr( # noqa: B010 + DatamodelClass.__schema_class__, "__make_object__", __make_object__ + ) + DatamodelClass._complete_datamodel_build() + + registry[name] = DatamodelClass + + return DatamodelClass + + @classmethod + def _complete_datamodel_build(cls): + """ 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. + + Nothing is done in the base Datamodel, but a Datamodel can inherit + the method to add its own behavior. + """ + pass + + +# makes the datamodels registry available on env + + +@property +def datamodels(self): + return _datamodel_databases.get(self.cr.dbname) + + +Environment.datamodels = datamodels diff --git a/datamodel/datamodels/__init__.py b/datamodel/datamodels/__init__.py new file mode 100644 index 000000000..0e4444933 --- /dev/null +++ b/datamodel/datamodels/__init__.py @@ -0,0 +1 @@ +from . import base diff --git a/datamodel/datamodels/base.py b/datamodel/datamodels/base.py new file mode 100644 index 000000000..82790d6d6 --- /dev/null +++ b/datamodel/datamodels/base.py @@ -0,0 +1,15 @@ +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from ..core import Datamodel + + +class BaseDatamodel(Datamodel): + """ This is the base datamodel for every datamodel + + It is implicitely inherited by all datamodels. + + All your base are belong to us + """ + + _name = "base" diff --git a/datamodel/fields.py b/datamodel/fields.py new file mode 100644 index 000000000..a193a4435 --- /dev/null +++ b/datamodel/fields.py @@ -0,0 +1,41 @@ +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +""" + +Fields +===== + +Create a single place for all fields defined for datamodels + +""" +import logging + +from .core import Datamodel + +_logger = logging.getLogger(__name__) + +try: + from marshmallow.fields import * # noqa: F403,F401 + from marshmallow.fields import Nested +except ImportError: + Nested = object + _logger.debug("Cannot import 'marshmallow'.") + + +class NestedModel(Nested): + def __init__(self, nested, **kwargs): + self.datamodel_name = nested + super(NestedModel, self).__init__(None, **kwargs) + + @property + def schema(self): + if not self.nested: + self.nested = self.context["_registry"][ + self.datamodel_name + ].__schema_class__() + return super(NestedModel, self).schema + + def _deserialize(self, value, attr, data, **kwargs): + if isinstance(value, Datamodel): + return value + return super(NestedModel, self)._deserialize(value, attr, data, **kwargs) diff --git a/datamodel/static/description/icon.png b/datamodel/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 diff --git a/datamodel/tests/__init__.py b/datamodel/tests/__init__.py new file mode 100644 index 000000000..e43f8bf6a --- /dev/null +++ b/datamodel/tests/__init__.py @@ -0,0 +1 @@ +from . import test_build_datamodel diff --git a/datamodel/tests/common.py b/datamodel/tests/common.py new file mode 100644 index 000000000..188e7af56 --- /dev/null +++ b/datamodel/tests/common.py @@ -0,0 +1,221 @@ +# Copyright 2017 Camptocamp SA +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import copy +import unittest +from contextlib import contextmanager + +import odoo +from odoo import api +from odoo.tests import common + +from ..core import DatamodelRegistry, MetaDatamodel, _get_addon_name + + +@contextmanager +def new_rollbacked_env(): + registry = odoo.registry(common.get_db_name()) + uid = odoo.SUPERUSER_ID + cr = registry.cursor() + try: + yield api.Environment(cr, uid, {}) + finally: + cr.rollback() # we shouldn't have to commit anything + cr.close() + + +class DatamodelMixin(object): + @classmethod + def setUpDatamodel(cls): + with new_rollbacked_env() as env: + builder = env["datamodel.builder"] + # build the datamodels of every installed addons + datamodel_registry = builder._init_global_registry() + cls._datamodels_registry = datamodel_registry + # ensure that we load only the datamodels of the 'installed' + # modules, not 'to install', which means we load only the + # dependencies of the tested addons, not the siblings or + # chilren addons + builder.build_registry(datamodel_registry, states=("installed",)) + # build the datamodels of the current tested addon + current_addon = _get_addon_name(cls.__module__) + env["datamodel.builder"].load_datamodels(current_addon) + + # pylint: disable=W8106 + def setUp(self): + # should be ready only during tests, never during installation + # of addons + self._datamodels_registry.ready = True + + @self.addCleanup + def notready(): + self._datamodels_registry.ready = False + + +class TransactionDatamodelCase(common.TransactionCase, DatamodelMixin): + """ A TransactionCase that loads all the datamodels + + It it used like an usual Odoo's TransactionCase, but it ensures + that all the datamodels of the current addon and its dependencies + are loaded. + + """ + + @classmethod + def setUpClass(cls): + super(TransactionDatamodelCase, cls).setUpClass() + cls.setUpDatamodel() + + # pylint: disable=W8106 + def setUp(self): + # resolve an inheritance issue (common.TransactionCase does not call + # super) + common.TransactionCase.setUp(self) + DatamodelMixin.setUp(self) + + +class SavepointDatamodelCase(common.SavepointCase, DatamodelMixin): + """ A SavepointCase that loads all the datamodels + + It is used like an usual Odoo's SavepointCase, but it ensures + that all the datamodels of the current addon and its dependencies + are loaded. + + """ + + @classmethod + def setUpClass(cls): + super(SavepointDatamodelCase, cls).setUpClass() + cls.setUpDatamodel() + + # pylint: disable=W8106 + def setUp(self): + # resolve an inheritance issue (common.SavepointCase does not call + # super) + common.SavepointCase.setUp(self) + DatamodelMixin.setUp(self) + + +class DatamodelRegistryCase( + unittest.TestCase, common.MetaCase("DummyCase", (object,), {}) +): + """ This test case can be used as a base for writings tests on datamodels + + This test case is meant to test datamodels in a special datamodel registry, + where you want to have maximum control on which datamodels are loaded + or not, or when you want to create additional datamodels in your tests. + + If you only want to *use* the datamodels of the tested addon in your tests, + then consider using one of: + + * :class:`TransactionDatamodelCase` + * :class:`SavepointDatamodelCase` + + This test case creates a special + :class:`odoo.addons.datamodel.core.DatamodelRegistry` for the purpose of + the tests. By default, it loads all the datamodels of the dependencies, but + not the datamodels of the current addon (which you have to handle + manually). In your tests, you can add more datamodels in 2 manners. + + All the datamodels of an Odoo module:: + + self._load_module_datamodels('connector') + + Only specific datamodels:: + + self._build_datamodels(MyDatamodel1, MyDatamodel2) + + Note: for the lookups of the datamodels, the default datamodel + registry is a global registry for the database. Here, you will + need to explicitly pass ``self.datamodel_registry`` in the + :class:`~odoo.addons.datamodel.core.WorkContext`:: + + work = WorkContext(model_name='res.users', + collection='my.collection', + datamodels_registry=self.datamodel_registry) + + Or:: + + collection_record = self.env['my.collection'].browse(1) + with collection_record.work_on( + 'res.partner', + datamodels_registry=self.datamodel_registry) as work: + + """ + + def setUp(self): + super(DatamodelRegistryCase, self).setUp() + + # keep the original classes registered by the metaclass + # so we'll restore them at the end of the tests, it avoid + # to pollute it with Stub / Test datamodels + self._original_datamodels = copy.deepcopy(MetaDatamodel._modules_datamodels) + + # it will be our temporary datamodel registry for our test session + self.datamodel_registry = DatamodelRegistry() + + # it builds the 'final datamodel' for every datamodel of the + # 'datamodel' addon and push them in the datamodel registry + self.datamodel_registry.load_datamodels("datamodel") + # build the datamodels of every installed addons already installed + # but the current addon (when running with pytest/nosetest, we + # simulate the --test-enable behavior by excluding the current addon + # which is in 'to install' / 'to upgrade' with --test-enable). + current_addon = _get_addon_name(self.__module__) + with new_rollbacked_env() as env: + env["datamodel.builder"].build_registry( + self.datamodel_registry, + states=("installed",), + exclude_addons=[current_addon], + ) + + # Fake that we are ready to work with the registry + # normally, it is set to True and the end of the build + # of the datamodels. Here, we'll add datamodels later in + # the datamodels registry, but we don't mind for the tests. + self.datamodel_registry.ready = True + + def tearDown(self): + super(DatamodelRegistryCase, self).tearDown() + # restore the original metaclass' classes + MetaDatamodel._modules_datamodels = self._original_datamodels + + def _load_module_datamodels(self, module): + self.datamodel_registry.load_datamodels(module) + + def _build_datamodels(self, *classes): + for cls in classes: + cls._build_datamodel(self.datamodel_registry) + + +class TransactionDatamodelRegistryCase(common.TransactionCase, DatamodelRegistryCase): + """ Adds Odoo Transaction in the base Datamodel TestCase """ + + # pylint: disable=W8106 + def setUp(self): + # resolve an inheritance issue (common.TransactionCase does not use + # super) + common.TransactionCase.setUp(self) + DatamodelRegistryCase.setUp(self) + self.collection = self.env["collection.base"] + + def teardown(self): + common.TransactionCase.tearDown(self) + DatamodelRegistryCase.tearDown(self) + + +class SavepointDatamodelRegistryCase(common.SavepointCase, DatamodelRegistryCase): + """ Adds Odoo Transaction with Savepoint in the base Datamodel TestCase """ + + # pylint: disable=W8106 + def setUp(self): + # resolve an inheritance issue (common.SavepointCase does not use + # super) + common.SavepointCase.setUp(self) + DatamodelRegistryCase.setUp(self) + self.collection = self.env["collection.base"] + + def teardown(self): + common.SavepointCase.tearDown(self) + DatamodelRegistryCase.tearDown(self) diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py new file mode 100644 index 000000000..54051c8c6 --- /dev/null +++ b/datamodel/tests/test_build_datamodel.py @@ -0,0 +1,407 @@ +# Copyright 2017 Camptocamp SA +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import mock +from marshmallow_objects.models import Model as MarshmallowModel + +from .. import fields +from ..core import Datamodel, _datamodel_databases +from .common import DatamodelRegistryCase, TransactionDatamodelCase + + +class TestBuildDatamodel(DatamodelRegistryCase): + """ Test build of datamodels + + All the tests in this suite are based on the same principle with + variations: + + * Create new Datamodels (classes inheriting from + :class:`datamodel.core.Datamodel` + * Call :meth:`datamodel.core.Datamodel._build_datamodel` on them + in order to build the 'final class' composed from all the ``_inherit`` + and push it in the datamodels registry (``self.datamodel_registry`` here) + * Assert that classes are built, registered, have correct ``__bases__``... + + """ + + def test_type(self): + """ Ensure that a datamodels are instances of + marshomallow_objects.Model""" + + class Datamodel1(Datamodel): + _name = "datamodel1" + + class Datamodel2(Datamodel): + _name = "datamodel2" + _inherit = "datamodel1" + + self.assertIsInstance(Datamodel1(), MarshmallowModel) + self.assertIsInstance(Datamodel2(), MarshmallowModel) + + def test_no_name(self): + """ Ensure that a datamodel has a _name """ + + class Datamodel1(Datamodel): + pass + + msg = ".*must have a _name.*" + with self.assertRaisesRegex(TypeError, msg): + Datamodel1._build_datamodel(self.datamodel_registry) + + def test_register(self): + """ Able to register datamodels in datamodels registry """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + + class Datamodel2(Datamodel): + _name = "datamodel2" + + # build the 'final classes' for the datamodels and check that we find + # them in the datamodels registry + Datamodel1._build_datamodel(self.datamodel_registry) + Datamodel2._build_datamodel(self.datamodel_registry) + self.assertEqual( + ["base", "datamodel1", "datamodel2"], list(self.datamodel_registry) + ) + + def test_inherit_bases(self): + """ Check __bases__ of Datamodel with _inherit """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + field_str1 = fields.String(missing="field_str1") + + class Datamodel2(Datamodel): + _inherit = "datamodel1" + field_str2 = fields.String(missing="field_str2") + + class Datamodel3(Datamodel): + _inherit = "datamodel1" + field_str3 = fields.String(missing="field_str3") + + Datamodel1._build_datamodel(self.datamodel_registry) + Datamodel2._build_datamodel(self.datamodel_registry) + Datamodel3._build_datamodel(self.datamodel_registry) + self.assertEqual( + (Datamodel3, Datamodel2, Datamodel1, self.datamodel_registry["base"]), + self.datamodel_registry["datamodel1"].__bases__, + ) + + def test_prototype_inherit_bases(self): + """ Check __bases__ of Datamodel with _inherit and different _name """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + field_int = fields.Int(missing=1) + + class Datamodel2(Datamodel): + _name = "datamodel2" + _inherit = "datamodel1" + field_boolean = fields.Boolean(missing=True) + field_int = fields.Int(missing=2) + + class Datamodel3(Datamodel): + _name = "datamodel3" + _inherit = "datamodel1" + field_float = fields.Float(missing=0.3) + + class Datamodel4(Datamodel): + _name = "datamodel4" + _inherit = ["datamodel2", "datamodel3"] + + Datamodel1._build_datamodel(self.datamodel_registry) + Datamodel2._build_datamodel(self.datamodel_registry) + Datamodel3._build_datamodel(self.datamodel_registry) + Datamodel4._build_datamodel(self.datamodel_registry) + self.assertEqual( + (Datamodel1, self.datamodel_registry["base"]), + self.datamodel_registry["datamodel1"].__bases__, + ) + self.assertEqual( + ( + Datamodel2, + self.datamodel_registry["datamodel1"], + self.datamodel_registry["base"], + ), + self.datamodel_registry["datamodel2"].__bases__, + ) + self.assertEqual( + ( + Datamodel3, + self.datamodel_registry["datamodel1"], + self.datamodel_registry["base"], + ), + self.datamodel_registry["datamodel3"].__bases__, + ) + self.assertEqual( + ( + Datamodel4, + self.datamodel_registry["datamodel2"], + self.datamodel_registry["datamodel3"], + self.datamodel_registry["base"], + ), + self.datamodel_registry["datamodel4"].__bases__, + ) + + def test_final_class_schema(self): + """Check the MArshmallow schema of the final class""" + + class Datamodel1(Datamodel): + _name = "datamodel1" + field_int = fields.Int(missing=1) + + class Datamodel2(Datamodel): + _name = "datamodel2" + _inherit = "datamodel1" + field_boolean = fields.Boolean(missing=True) + field_int = fields.Int(missing=2) + + class Datamodel3(Datamodel): + _name = "datamodel3" + _inherit = "datamodel1" + field_float = fields.Float(missing=0.3) + + class Datamodel4(Datamodel): + _name = "datamodel4" + _inherit = ["datamodel2", "datamodel3"] + + Datamodel1._build_datamodel(self.datamodel_registry) + Datamodel2._build_datamodel(self.datamodel_registry) + Datamodel3._build_datamodel(self.datamodel_registry) + Datamodel4._build_datamodel(self.datamodel_registry) + + Datamodel1 = self.datamodel_registry["datamodel1"] + Datamodel2 = self.datamodel_registry["datamodel2"] + Datamodel3 = self.datamodel_registry["datamodel3"] + Datamodel4 = self.datamodel_registry["datamodel4"] + + self.assertEqual(Datamodel1().dump(), {"field_int": 1}) + self.assertDictEqual( + Datamodel2().dump(), {"field_boolean": True, "field_int": 2} + ) + self.assertDictEqual(Datamodel3().dump(), {"field_float": 0.3, "field_int": 1}) + self.assertDictEqual( + Datamodel4().dump(), + {"field_boolean": True, "field_int": 2, "field_float": 0.3}, + ) + + def test_custom_build(self): + """ Check that we can hook at the end of a Datamodel build """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + + @classmethod + def _complete_datamodel_build(cls): + # This method should be called after the Datamodel + # is built, and before it is pushed in the registry + cls._build_done = True + + Datamodel1._build_datamodel(self.datamodel_registry) + # we inspect that our custom build has been executed + self.assertTrue(self.datamodel_registry["datamodel1"]._build_done) + + def test_inherit_attrs(self): + """ Check attributes inheritance of Datamodels with _inherit """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + + msg = "ping" + + def say(self): + return "foo" + + class Datamodel2(Datamodel): + _name = "datamodel2" + _inherit = "datamodel1" + + msg = "pong" + + def say(self): + return super(Datamodel2, self).say() + " bar" + + Datamodel1._build_datamodel(self.datamodel_registry) + Datamodel2._build_datamodel(self.datamodel_registry) + # we initialize the datamodels, normally we should pass + # an instance of WorkContext, but we don't need a real one + # for this test + datamodel1 = self.datamodel_registry["datamodel1"](mock.Mock()) + datamodel2 = self.datamodel_registry["datamodel2"](mock.Mock()) + self.assertEqual("ping", datamodel1.msg) + self.assertEqual("pong", datamodel2.msg) + self.assertEqual("foo", datamodel1.say()) + self.assertEqual("foo bar", datamodel2.say()) + + def test_duplicate_datamodel(self): + """ Check that we can't have 2 datamodels with the same name """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + + class Datamodel2(Datamodel): + _name = "datamodel1" + + Datamodel1._build_datamodel(self.datamodel_registry) + msg = "Datamodel.*already exists.*" + with self.assertRaisesRegex(TypeError, msg): + Datamodel2._build_datamodel(self.datamodel_registry) + + def test_no_parent(self): + """ Ensure we can't _inherit a non-existent datamodel """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + _inherit = "datamodel1" + + msg = "Datamodel.*does not exist in registry.*" + with self.assertRaisesRegex(TypeError, msg): + Datamodel1._build_datamodel(self.datamodel_registry) + + def test_no_parent2(self): + """ Ensure we can't _inherit by prototype a non-existent datamodel """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + + class Datamodel2(Datamodel): + _name = "datamodel2" + _inherit = ["datamodel1", "datamodel3"] + + Datamodel1._build_datamodel(self.datamodel_registry) + msg = "Datamodel.*inherits from non-existing datamodel.*" + with self.assertRaisesRegex(TypeError, msg): + Datamodel2._build_datamodel(self.datamodel_registry) + + def test_add_inheritance(self): + """ Ensure we can add a new inheritance """ + + class Datamodel1(Datamodel): + _name = "datamodel1" + + class Datamodel2(Datamodel): + _name = "datamodel2" + + class Datamodel2bis(Datamodel): + _name = "datamodel2" + _inherit = ["datamodel2", "datamodel1"] + + Datamodel1._build_datamodel(self.datamodel_registry) + Datamodel2._build_datamodel(self.datamodel_registry) + Datamodel2bis._build_datamodel(self.datamodel_registry) + + self.assertEqual( + ( + Datamodel2bis, + Datamodel2, + self.datamodel_registry["datamodel1"], + self.datamodel_registry["base"], + ), + self.datamodel_registry["datamodel2"].__bases__, + ) + + def test_add_inheritance_final_schema(self): + """ Ensure that the Marshmallow schema is updated if we add a + new inheritance""" + + class Datamodel1(Datamodel): + _name = "datamodel1" + field_str1 = fields.String(missing="str1") + + class Datamodel2(Datamodel): + _name = "datamodel2" + field_str2 = fields.String(missing="str2") + + class Datamodel2bis(Datamodel): + _name = "datamodel2" + _inherit = ["datamodel2", "datamodel1"] + field_str3 = fields.String(missing="str3") + + Datamodel1._build_datamodel(self.datamodel_registry) + Datamodel2._build_datamodel(self.datamodel_registry) + Datamodel2bis._build_datamodel(self.datamodel_registry) + + Datamodel2 = self.datamodel_registry["datamodel2"] + self.assertDictEqual( + Datamodel2().dump(), + {"field_str1": "str1", "field_str2": "str2", "field_str3": "str3"}, + ) + + def test_nested_model(self): + """ Test nested model serialization/deserialization""" + + class Parent(Datamodel): + _name = "parent" + name = fields.String() + child = fields.NestedModel("child") + + class Child(Datamodel): + _name = "child" + field_str = fields.String() + + Parent._build_datamodel(self.datamodel_registry) + Child._build_datamodel(self.datamodel_registry) + + Parent = self.datamodel_registry["parent"] + Child = self.datamodel_registry["child"] + + instance = Parent(name="Parent", child=Child(field_str="My other string")) + res = instance.dump() + self.assertDictEqual( + res, {"child": {"field_str": "My other string"}, "name": "Parent"} + ) + new_instance = instance.load(res) + self.assertEqual(new_instance.name, instance.name) + self.assertEqual(new_instance.child.field_str, instance.child.field_str) + + def test_many(self): + """Test loads of many""" + + class Item(Datamodel): + _name = "item" + idx = fields.Integer() + + Item._build_datamodel(self.datamodel_registry) + Item = self.datamodel_registry["item"] + + items = Item.load([{"idx": 1}, {"idx": 2}], many=True) + self.assertTrue(len(items), 2) + self.assertEqual([i.idx for i in items], [1, 2]) + + def test_nested_many(self): + """Tests loads and dump of model with array of nested model""" + + class Parent(Datamodel): + _name = "parent" + items = fields.NestedModel("item", many=True) + + class Item(Datamodel): + _name = "item" + idx = fields.Integer() + + Parent._build_datamodel(self.datamodel_registry) + Item._build_datamodel(self.datamodel_registry) + + Parent = self.datamodel_registry["parent"] + Item = self.datamodel_registry["item"] + + instance = Parent.load({"items": [{"idx": 1}, {"idx": 2}]}) + res = instance.dump() + self.assertEqual(res, {"items": [{"idx": 1}, {"idx": 2}]}) + new_instance = Parent.load(res) + self.assertEqual(len(new_instance.items), 2) + self.assertEqual([i.idx for i in new_instance.items], [1, 2]) + new_instance.items.append(Item(idx=3)) + res = new_instance.dump() + self.assertEqual(res, {"items": [{"idx": 1}, {"idx": 2}, {"idx": 3}]}) + + +class TestRegistryAccess(TransactionDatamodelCase): + def test_registry_access(self): + """Check the access to the registry directly on tnv""" + registry = self.env.datamodels + self.assertEqual(registry, _datamodel_databases[self.env.cr.dbname]) + base = self.env.datamodels["base"] + self.assertIsInstance(base(), MarshmallowModel) From 67745d2f5efcf20d407e598f582df671208f6bdf Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 8 Oct 2019 21:01:25 +0200 Subject: [PATCH 02/37] datamodel: registry is always available a class level We no more need to get the registry from the context since it's always available on the schema class --- datamodel/core.py | 17 +++++++---------- datamodel/fields.py | 4 +--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/datamodel/core.py b/datamodel/core.py index f8f744e27..0fda0aab4 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -188,22 +188,19 @@ class AnotherDatamodel(Datamodel): #: Name or list of names of the datamodel(s) to inherit from _inherit = None - def __init__(self, *args, **kwargs): - super(Datamodel, self).__init__(*args, **kwargs) - self.__schema__.context["_registry"] = self.registry - @property def registry(self): """ Current datamodels registry""" return self._registry @classmethod - def load(cls, data, context=None, many=None, partial=None): - ctx = (context or {}).copy() - ctx["_registry"] = cls._registry - return super(Datamodel, cls).load( - data=data, context=ctx, many=many, partial=partial - ) + def get_schema(cls, **kwargs): + """ + Get a marshmallow schema instance + :param kwargs: + :return: + """ + return cls.__get_schema_class__(**kwargs) @classmethod def _build_datamodel(cls, registry): diff --git a/datamodel/fields.py b/datamodel/fields.py index a193a4435..ad32d555a 100644 --- a/datamodel/fields.py +++ b/datamodel/fields.py @@ -30,9 +30,7 @@ def __init__(self, nested, **kwargs): @property def schema(self): if not self.nested: - self.nested = self.context["_registry"][ - self.datamodel_name - ].__schema_class__() + self.nested = self.parent._registry[self.datamodel_name].__schema_class__() return super(NestedModel, self).schema def _deserialize(self, value, attr, data, **kwargs): From 98179f9040a42fbf2b66d5700112bf640f24b8a9 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Sun, 3 May 2020 12:43:49 +0200 Subject: [PATCH 03/37] datamodel: schema property of NestedModel must be a class This change fixes the incompatibility with the latest marshmallow version --- datamodel/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/fields.py b/datamodel/fields.py index ad32d555a..28f59a6a8 100644 --- a/datamodel/fields.py +++ b/datamodel/fields.py @@ -30,7 +30,7 @@ def __init__(self, nested, **kwargs): @property def schema(self): if not self.nested: - self.nested = self.parent._registry[self.datamodel_name].__schema_class__() + self.nested = self.parent._registry[self.datamodel_name].__schema_class__ return super(NestedModel, self).schema def _deserialize(self, value, attr, data, **kwargs): From 2f7989142ad4a6bcb1ccb76ce511deea499a5ccd Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Sun, 3 May 2020 15:54:35 +0200 Subject: [PATCH 04/37] datamodels: ensures that the Odoo environment is always available as a property of datamodel instances as well as on marshmallow schema instances. --- datamodel/core.py | 44 ++++++++++-- datamodel/fields.py | 5 +- datamodel/tests/common.py | 43 ++++++------ datamodel/tests/test_build_datamodel.py | 90 ++++++++++++++++--------- 4 files changed, 123 insertions(+), 59 deletions(-) diff --git a/datamodel/core.py b/datamodel/core.py index 0fda0aab4..73f3b9451 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -2,6 +2,7 @@ # Copyright 2019 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import functools import logging from collections import OrderedDict, defaultdict @@ -92,7 +93,7 @@ def load_datamodels(self, module): @marshmallow.post_load def __make_object__(self, data, **kwargs): - datamodel = self._registry[self._datamodel_name] + datamodel = self._env.datamodels[self._datamodel_name] return datamodel(__post_load__=True, __schema__=self, **data) @@ -180,7 +181,7 @@ class AnotherDatamodel(Datamodel): """ _register = False - _registry = None # DatamodelResgistry initialized by the metaclass + _env = None # Odoo Environment # used for inheritance _name = None #: Name of the datamodel @@ -188,10 +189,14 @@ class AnotherDatamodel(Datamodel): #: Name or list of names of the datamodel(s) to inherit from _inherit = None + def __init__(self, context=None, partial=None, env=None, **kwargs): + self._env = env + super().__init__(context=context, partial=partial, **kwargs) + @property - def registry(self): + def env(self): """ Current datamodels registry""" - return self._registry + return self._env @classmethod def get_schema(cls, **kwargs): @@ -361,7 +366,36 @@ def _complete_datamodel_build(cls): @property def datamodels(self): - return _datamodel_databases.get(self.cr.dbname) + class DataModelFactory(object): + """Factory for datamodels + + This factory ensures the propagation of the environment to the + instanciated datamodels and related schema. + """ + + __slots__ = ("env", "registry") + + def __init__(self, env, registry): + self.env = env + self.registry = registry + + def __getitem__(self, key): + model = self.registry[key] + model.__init__ = functools.partialmethod(model.__init__, env=self.env) + + @classmethod + def __get_schema_class__(cls, **kwargs): + cls = cls.__schema_class__(**kwargs) + cls._env = self.env + return cls + + model.__get_schema_class__ = __get_schema_class__ + return model + + if not hasattr(self, "_datamodels_factory"): + factory = DataModelFactory(self, _datamodel_databases.get(self.cr.dbname)) + self._datamodels_factory = factory + return self._datamodels_factory Environment.datamodels = datamodels diff --git a/datamodel/fields.py b/datamodel/fields.py index 28f59a6a8..5615087b3 100644 --- a/datamodel/fields.py +++ b/datamodel/fields.py @@ -30,7 +30,10 @@ def __init__(self, nested, **kwargs): @property def schema(self): if not self.nested: - self.nested = self.parent._registry[self.datamodel_name].__schema_class__ + self.nested = self.parent._env.datamodels[ + self.datamodel_name + ].__schema_class__ + self.nested._env = self.parent._env return super(NestedModel, self).schema def _deserialize(self, value, attr, data, **kwargs): diff --git a/datamodel/tests/common.py b/datamodel/tests/common.py index 188e7af56..d1453ffa4 100644 --- a/datamodel/tests/common.py +++ b/datamodel/tests/common.py @@ -10,7 +10,12 @@ from odoo import api from odoo.tests import common -from ..core import DatamodelRegistry, MetaDatamodel, _get_addon_name +from ..core import ( + DatamodelRegistry, + MetaDatamodel, + _datamodel_databases, + _get_addon_name, +) @contextmanager @@ -129,19 +134,6 @@ class DatamodelRegistryCase( Note: for the lookups of the datamodels, the default datamodel registry is a global registry for the database. Here, you will need to explicitly pass ``self.datamodel_registry`` in the - :class:`~odoo.addons.datamodel.core.WorkContext`:: - - work = WorkContext(model_name='res.users', - collection='my.collection', - datamodels_registry=self.datamodel_registry) - - Or:: - - collection_record = self.env['my.collection'].browse(1) - with collection_record.work_on( - 'res.partner', - datamodels_registry=self.datamodel_registry) as work: - """ def setUp(self): @@ -163,12 +155,23 @@ def setUp(self): # simulate the --test-enable behavior by excluding the current addon # which is in 'to install' / 'to upgrade' with --test-enable). current_addon = _get_addon_name(self.__module__) - with new_rollbacked_env() as env: - env["datamodel.builder"].build_registry( - self.datamodel_registry, - states=("installed",), - exclude_addons=[current_addon], - ) + + registry = odoo.registry(common.get_db_name()) + uid = odoo.SUPERUSER_ID + cr = registry.cursor() + env = api.Environment(cr, uid, {}) + env["datamodel.builder"].build_registry( + self.datamodel_registry, + states=("installed",), + exclude_addons=[current_addon], + ) + self.env = env + _datamodel_databases[self.env.cr.dbname] = self.datamodel_registry + + @self.addCleanup + def _close_and_roolback(): + cr.rollback() # we shouldn't have to commit anything + cr.close() # Fake that we are ready to work with the registry # normally, it is set to True and the end of the build diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index 54051c8c6..7310f39b7 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -6,7 +6,7 @@ from marshmallow_objects.models import Model as MarshmallowModel from .. import fields -from ..core import Datamodel, _datamodel_databases +from ..core import Datamodel from .common import DatamodelRegistryCase, TransactionDatamodelCase @@ -85,8 +85,8 @@ class Datamodel3(Datamodel): Datamodel2._build_datamodel(self.datamodel_registry) Datamodel3._build_datamodel(self.datamodel_registry) self.assertEqual( - (Datamodel3, Datamodel2, Datamodel1, self.datamodel_registry["base"]), - self.datamodel_registry["datamodel1"].__bases__, + (Datamodel3, Datamodel2, Datamodel1, self.env.datamodels["base"]), + self.env.datamodels["datamodel1"].__bases__, ) def test_prototype_inherit_bases(self): @@ -116,33 +116,33 @@ class Datamodel4(Datamodel): Datamodel3._build_datamodel(self.datamodel_registry) Datamodel4._build_datamodel(self.datamodel_registry) self.assertEqual( - (Datamodel1, self.datamodel_registry["base"]), - self.datamodel_registry["datamodel1"].__bases__, + (Datamodel1, self.env.datamodels["base"]), + self.env.datamodels["datamodel1"].__bases__, ) self.assertEqual( ( Datamodel2, - self.datamodel_registry["datamodel1"], - self.datamodel_registry["base"], + self.env.datamodels["datamodel1"], + self.env.datamodels["base"], ), - self.datamodel_registry["datamodel2"].__bases__, + self.env.datamodels["datamodel2"].__bases__, ) self.assertEqual( ( Datamodel3, - self.datamodel_registry["datamodel1"], - self.datamodel_registry["base"], + self.env.datamodels["datamodel1"], + self.env.datamodels["base"], ), - self.datamodel_registry["datamodel3"].__bases__, + self.env.datamodels["datamodel3"].__bases__, ) self.assertEqual( ( Datamodel4, - self.datamodel_registry["datamodel2"], - self.datamodel_registry["datamodel3"], - self.datamodel_registry["base"], + self.env.datamodels["datamodel2"], + self.env.datamodels["datamodel3"], + self.env.datamodels["base"], ), - self.datamodel_registry["datamodel4"].__bases__, + self.env.datamodels["datamodel4"].__bases__, ) def test_final_class_schema(self): @@ -172,10 +172,10 @@ class Datamodel4(Datamodel): Datamodel3._build_datamodel(self.datamodel_registry) Datamodel4._build_datamodel(self.datamodel_registry) - Datamodel1 = self.datamodel_registry["datamodel1"] - Datamodel2 = self.datamodel_registry["datamodel2"] - Datamodel3 = self.datamodel_registry["datamodel3"] - Datamodel4 = self.datamodel_registry["datamodel4"] + Datamodel1 = self.env.datamodels["datamodel1"] + Datamodel2 = self.env.datamodels["datamodel2"] + Datamodel3 = self.env.datamodels["datamodel3"] + Datamodel4 = self.env.datamodels["datamodel4"] self.assertEqual(Datamodel1().dump(), {"field_int": 1}) self.assertDictEqual( @@ -201,7 +201,7 @@ def _complete_datamodel_build(cls): Datamodel1._build_datamodel(self.datamodel_registry) # we inspect that our custom build has been executed - self.assertTrue(self.datamodel_registry["datamodel1"]._build_done) + self.assertTrue(self.env.datamodels["datamodel1"]._build_done) def test_inherit_attrs(self): """ Check attributes inheritance of Datamodels with _inherit """ @@ -228,8 +228,8 @@ def say(self): # we initialize the datamodels, normally we should pass # an instance of WorkContext, but we don't need a real one # for this test - datamodel1 = self.datamodel_registry["datamodel1"](mock.Mock()) - datamodel2 = self.datamodel_registry["datamodel2"](mock.Mock()) + datamodel1 = self.env.datamodels["datamodel1"](mock.Mock()) + datamodel2 = self.env.datamodels["datamodel2"](mock.Mock()) self.assertEqual("ping", datamodel1.msg) self.assertEqual("pong", datamodel2.msg) self.assertEqual("foo", datamodel1.say()) @@ -296,10 +296,10 @@ class Datamodel2bis(Datamodel): ( Datamodel2bis, Datamodel2, - self.datamodel_registry["datamodel1"], - self.datamodel_registry["base"], + self.env.datamodels["datamodel1"], + self.env.datamodels["base"], ), - self.datamodel_registry["datamodel2"].__bases__, + self.env.datamodels["datamodel2"].__bases__, ) def test_add_inheritance_final_schema(self): @@ -323,7 +323,7 @@ class Datamodel2bis(Datamodel): Datamodel2._build_datamodel(self.datamodel_registry) Datamodel2bis._build_datamodel(self.datamodel_registry) - Datamodel2 = self.datamodel_registry["datamodel2"] + Datamodel2 = self.env.datamodels["datamodel2"] self.assertDictEqual( Datamodel2().dump(), {"field_str1": "str1", "field_str2": "str2", "field_str3": "str3"}, @@ -344,8 +344,8 @@ class Child(Datamodel): Parent._build_datamodel(self.datamodel_registry) Child._build_datamodel(self.datamodel_registry) - Parent = self.datamodel_registry["parent"] - Child = self.datamodel_registry["child"] + Parent = self.env.datamodels["parent"] + Child = self.env.datamodels["child"] instance = Parent(name="Parent", child=Child(field_str="My other string")) res = instance.dump() @@ -364,7 +364,7 @@ class Item(Datamodel): idx = fields.Integer() Item._build_datamodel(self.datamodel_registry) - Item = self.datamodel_registry["item"] + Item = self.env.datamodels["item"] items = Item.load([{"idx": 1}, {"idx": 2}], many=True) self.assertTrue(len(items), 2) @@ -384,8 +384,8 @@ class Item(Datamodel): Parent._build_datamodel(self.datamodel_registry) Item._build_datamodel(self.datamodel_registry) - Parent = self.datamodel_registry["parent"] - Item = self.datamodel_registry["item"] + Parent = self.env.datamodels["parent"] + Item = self.env.datamodels["item"] instance = Parent.load({"items": [{"idx": 1}, {"idx": 2}]}) res = instance.dump() @@ -397,11 +397,35 @@ class Item(Datamodel): res = new_instance.dump() self.assertEqual(res, {"items": [{"idx": 1}, {"idx": 2}, {"idx": 3}]}) + def test_env(self): + """ + Tests that the current env is always available on datamodel instances + and schema + """ + + class Parent(Datamodel): + _name = "parent" + items = fields.NestedModel("item", many=True) + + class Item(Datamodel): + _name = "item" + idx = fields.Integer() + + Parent._build_datamodel(self.datamodel_registry) + Item._build_datamodel(self.datamodel_registry) + Parent = self.env.datamodels["parent"] + p = Parent() + self.assertEqual(p.env, self.env) + schema = Parent.get_schema() + self.assertEqual(schema._env, self.env) + instance = Parent.load({"items": [{"idx": 1}, {"idx": 2}]}) + self.assertEqual(instance.items[0].env, self.env) + schema = instance.items[0].get_schema() + self.assertEqual(schema._env, self.env) + class TestRegistryAccess(TransactionDatamodelCase): def test_registry_access(self): """Check the access to the registry directly on tnv""" - registry = self.env.datamodels - self.assertEqual(registry, _datamodel_databases[self.env.cr.dbname]) base = self.env.datamodels["base"] self.assertIsInstance(base(), MarshmallowModel) From bfbe2ee2fdb23d905f0e9aab4635ce4f5b4a348b Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 26 May 2020 08:21:43 +0200 Subject: [PATCH 05/37] [MIG] datamodel: Migrate from 12.0 --- datamodel/__manifest__.py | 2 +- datamodel/builder.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 762078f1c..f6ab7a4d9 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "12.0.1.0.0", + "version": "13.0.1.0.0", "license": "AGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", diff --git a/datamodel/builder.py b/datamodel/builder.py index c73dcd1fd..8ae41f21e 100644 --- a/datamodel/builder.py +++ b/datamodel/builder.py @@ -10,7 +10,7 @@ Build the datamodels at the build of a registry. """ -from odoo import api, models, modules +from odoo import models, modules from .core import DEFAULT_CACHE_SIZE, DatamodelRegistry, _datamodel_databases @@ -40,7 +40,6 @@ class DatamodelBuilder(models.AbstractModel): _datamodels_registry_cache_size = DEFAULT_CACHE_SIZE - @api.model_cr def _register_hook(self): # This method is called by Odoo when the registry is built, # so in case the registry is rebuilt (cache invalidation, ...), From 97abd44a894276cb685cfb7fda26631e898d45e7 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 22 Dec 2020 13:40:41 +0100 Subject: [PATCH 06/37] [IMP] base_rest, datamode: Spelling --- datamodel/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/core.py b/datamodel/core.py index 73f3b9451..da59c0425 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -146,7 +146,7 @@ class Datamodel(MarshmallowModel, metaclass=MetaDatamodel): :: from marshmallow import fields - from odoo.addons.datamode.core import Datamodel + from odoo.addons.datamodel.core import Datamodel class MyDatamodel(Datamodel): _name = 'my.datamodel' From 37c509ee7e36db74f0a327bc1f93f87fb014ee77 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 22 Dec 2020 13:43:26 +0100 Subject: [PATCH 07/37] [IMP] datamodel: Move DataModelFactory class definition outside the datamodels method implementation --- datamodel/core.py | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/datamodel/core.py b/datamodel/core.py index da59c0425..0b8f5cf87 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -364,34 +364,35 @@ def _complete_datamodel_build(cls): # makes the datamodels registry available on env -@property -def datamodels(self): - class DataModelFactory(object): - """Factory for datamodels +class DataModelFactory(object): + """Factory for datamodels - This factory ensures the propagation of the environment to the - instanciated datamodels and related schema. - """ + This factory ensures the propagation of the environment to the + instanciated datamodels and related schema. + """ - __slots__ = ("env", "registry") + __slots__ = ("env", "registry") - def __init__(self, env, registry): - self.env = env - self.registry = registry + def __init__(self, env, registry): + self.env = env + self.registry = registry - def __getitem__(self, key): - model = self.registry[key] - model.__init__ = functools.partialmethod(model.__init__, env=self.env) + def __getitem__(self, key): + model = self.registry[key] + model.__init__ = functools.partialmethod(model.__init__, env=self.env) + + @classmethod + def __get_schema_class__(cls, **kwargs): + cls = cls.__schema_class__(**kwargs) + cls._env = self.env + return cls - @classmethod - def __get_schema_class__(cls, **kwargs): - cls = cls.__schema_class__(**kwargs) - cls._env = self.env - return cls + model.__get_schema_class__ = __get_schema_class__ + return model - model.__get_schema_class__ = __get_schema_class__ - return model +@property +def datamodels(self): if not hasattr(self, "_datamodels_factory"): factory = DataModelFactory(self, _datamodel_databases.get(self.cr.dbname)) self._datamodels_factory = factory From 739f6e84ac9b77a9530a07bfd8180f0d8bc73163 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 24 Dec 2020 15:03:46 +0100 Subject: [PATCH 08/37] fix website into __manifest__.py --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index f6ab7a4d9..5ff1258ad 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -11,7 +11,7 @@ "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", "maintainers": ["lmignon"], - "website": "https://acsone.eu/", + "website": "https://github.com/OCA/rest-framework", "depends": [], "data": [], "demo": [], From e3d6a4f35fd78608842812ee98f47b3dc3195019 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 24 Dec 2020 15:48:02 +0100 Subject: [PATCH 09/37] [MIG] Adapt code for 14.0 --- datamodel/__manifest__.py | 5 +++-- datamodel/tests/common.py | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 5ff1258ad..164682527 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "13.0.1.0.0", + "version": "14.0.1.0.0", "license": "AGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", @@ -15,5 +15,6 @@ "depends": [], "data": [], "demo": [], - "external_dependencies": {"python": ["marshmallow", "marshmallow_objects"]}, + "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, + "installable": True, } diff --git a/datamodel/tests/common.py b/datamodel/tests/common.py index d1453ffa4..016303b87 100644 --- a/datamodel/tests/common.py +++ b/datamodel/tests/common.py @@ -3,7 +3,6 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import copy -import unittest from contextlib import contextmanager import odoo @@ -103,7 +102,7 @@ def setUp(self): class DatamodelRegistryCase( - unittest.TestCase, common.MetaCase("DummyCase", (object,), {}) + common.TreeCase, common.MetaCase("DummyCase", (object,), {}) ): """ This test case can be used as a base for writings tests on datamodels From 755eaedf1bd335e54a940e057947d76f2229629b Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 24 Dec 2020 16:02:55 +0100 Subject: [PATCH 10/37] [FIX] pre-commit --- datamodel/builder.py | 4 ++-- datamodel/core.py | 11 +++++------ datamodel/datamodels/base.py | 2 +- datamodel/tests/common.py | 6 +++--- datamodel/tests/test_build_datamodel.py | 6 +++--- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/datamodel/builder.py b/datamodel/builder.py index 8ae41f21e..5ae5631c8 100644 --- a/datamodel/builder.py +++ b/datamodel/builder.py @@ -16,7 +16,7 @@ class DatamodelBuilder(models.AbstractModel): - """ Build the datamodel classes + """Build the datamodel classes And register them in a global registry. @@ -79,7 +79,7 @@ def build_registry(self, datamodels_registry, states=None, exclude_addons=None): self.load_datamodels(module.name, datamodels_registry=datamodels_registry) def load_datamodels(self, module, datamodels_registry=None): - """ Build every datamodel known by MetaDatamodel for an odoo module + """Build every datamodel known by MetaDatamodel for an odoo module The final datamodel (composed by all the Datamodel classes in this module) will be pushed into the registry. diff --git a/datamodel/core.py b/datamodel/core.py index 0b8f5cf87..c3a60bfa3 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -46,7 +46,7 @@ class DatamodelDatabases(dict): 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. @@ -98,7 +98,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. @@ -133,7 +133,7 @@ def __init__(self, name, bases, attrs): class Datamodel(MarshmallowModel, metaclass=MetaDatamodel): - """ Main Datamodel Model + """Main Datamodel Model All datamodels have a Python inheritance either on :class:`Datamodel`. @@ -209,7 +209,7 @@ def get_schema(cls, **kwargs): @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`. @@ -350,7 +350,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. @@ -358,7 +358,6 @@ def _complete_datamodel_build(cls): Nothing is done in the base Datamodel, but a Datamodel can inherit the method to add its own behavior. """ - pass # makes the datamodels registry available on env diff --git a/datamodel/datamodels/base.py b/datamodel/datamodels/base.py index 82790d6d6..3b96bc6ab 100644 --- a/datamodel/datamodels/base.py +++ b/datamodel/datamodels/base.py @@ -5,7 +5,7 @@ class BaseDatamodel(Datamodel): - """ This is the base datamodel for every datamodel + """This is the base datamodel for every datamodel It is implicitely inherited by all datamodels. diff --git a/datamodel/tests/common.py b/datamodel/tests/common.py index 016303b87..726980013 100644 --- a/datamodel/tests/common.py +++ b/datamodel/tests/common.py @@ -58,7 +58,7 @@ def notready(): class TransactionDatamodelCase(common.TransactionCase, DatamodelMixin): - """ A TransactionCase that loads all the datamodels + """A TransactionCase that loads all the datamodels It it used like an usual Odoo's TransactionCase, but it ensures that all the datamodels of the current addon and its dependencies @@ -80,7 +80,7 @@ def setUp(self): class SavepointDatamodelCase(common.SavepointCase, DatamodelMixin): - """ A SavepointCase that loads all the datamodels + """A SavepointCase that loads all the datamodels It is used like an usual Odoo's SavepointCase, but it ensures that all the datamodels of the current addon and its dependencies @@ -104,7 +104,7 @@ def setUp(self): class DatamodelRegistryCase( common.TreeCase, common.MetaCase("DummyCase", (object,), {}) ): - """ This test case can be used as a base for writings tests on datamodels + """This test case can be used as a base for writings tests on datamodels This test case is meant to test datamodels in a special datamodel registry, where you want to have maximum control on which datamodels are loaded diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index 7310f39b7..5aa48d599 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -11,7 +11,7 @@ class TestBuildDatamodel(DatamodelRegistryCase): - """ Test build of datamodels + """Test build of datamodels All the tests in this suite are based on the same principle with variations: @@ -26,7 +26,7 @@ class TestBuildDatamodel(DatamodelRegistryCase): """ def test_type(self): - """ Ensure that a datamodels are instances of + """Ensure that a datamodels are instances of marshomallow_objects.Model""" class Datamodel1(Datamodel): @@ -303,7 +303,7 @@ class Datamodel2bis(Datamodel): ) def test_add_inheritance_final_schema(self): - """ Ensure that the Marshmallow schema is updated if we add a + """Ensure that the Marshmallow schema is updated if we add a new inheritance""" class Datamodel1(Datamodel): From 73965b402f51ae725b6b06b6bbe47ebd541d51bd Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 15 Jan 2021 14:35:09 +0000 Subject: [PATCH 11/37] datamodel 14.0.2.0.0 --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 164682527..0fec57520 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "14.0.1.0.0", + "version": "14.0.2.0.0", "license": "AGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", From d2529e51e77e0a9847ff5e421ad297606eff6b11 Mon Sep 17 00:00:00 2001 From: David James Date: Fri, 18 Jun 2021 15:49:18 +1000 Subject: [PATCH 12/37] datamodel: Use more permissive licence: AGPL-> LGPL --- datamodel/__manifest__.py | 4 ++-- datamodel/builder.py | 2 +- datamodel/core.py | 2 +- datamodel/datamodels/base.py | 2 +- datamodel/fields.py | 2 +- datamodel/tests/common.py | 2 +- datamodel/tests/test_build_datamodel.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 0fec57520..86a2769ef 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -1,5 +1,5 @@ # Copyright 2019 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) { "name": "Datamodel", @@ -7,7 +7,7 @@ This addon allows you to define simple data models supporting serialization/deserialization""", "version": "14.0.2.0.0", - "license": "AGPL-3", + "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", "maintainers": ["lmignon"], diff --git a/datamodel/builder.py b/datamodel/builder.py index 5ae5631c8..59d29839a 100644 --- a/datamodel/builder.py +++ b/datamodel/builder.py @@ -1,6 +1,6 @@ # Copyright 2017 Camptocamp SA # Copyright 2019 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) """ diff --git a/datamodel/core.py b/datamodel/core.py index c3a60bfa3..8d0575df8 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -1,6 +1,6 @@ # Copyright 2017 Camptocamp SA # Copyright 2019 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) import functools import logging diff --git a/datamodel/datamodels/base.py b/datamodel/datamodels/base.py index 3b96bc6ab..1d0dc1225 100644 --- a/datamodel/datamodels/base.py +++ b/datamodel/datamodels/base.py @@ -1,5 +1,5 @@ # Copyright 2019 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) from ..core import Datamodel diff --git a/datamodel/fields.py b/datamodel/fields.py index 5615087b3..a2e52f006 100644 --- a/datamodel/fields.py +++ b/datamodel/fields.py @@ -1,5 +1,5 @@ # Copyright 2019 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) """ Fields diff --git a/datamodel/tests/common.py b/datamodel/tests/common.py index 726980013..1eb530654 100644 --- a/datamodel/tests/common.py +++ b/datamodel/tests/common.py @@ -1,6 +1,6 @@ # Copyright 2017 Camptocamp SA # Copyright 2019 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) import copy from contextlib import contextmanager diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index 5aa48d599..f87a18681 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -1,6 +1,6 @@ # Copyright 2017 Camptocamp SA # Copyright 2019 ACSONE SA/NV -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) import mock from marshmallow_objects.models import Model as MarshmallowModel From 062388e61b24fbb32f8f402363587c3917c86252 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 24 Jun 2021 10:31:09 +0000 Subject: [PATCH 13/37] datamodel 14.0.3.0.0 --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 86a2769ef..1e5cb38cd 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "14.0.2.0.0", + "version": "14.0.3.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", From 2c51c1da56f3739b5ff19fbe1b2f36b50605ca8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Thu, 18 Feb 2021 11:53:35 +0100 Subject: [PATCH 14/37] [BUG] issue with datamodel, recursion limit --- datamodel/tests/test_build_datamodel.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index f87a18681..616215cb0 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -329,6 +329,15 @@ class Datamodel2bis(Datamodel): {"field_str1": "str1", "field_str2": "str2", "field_str3": "str3"}, ) + def test_recursion(self): + class Datamodel1(Datamodel): + _name = "datamodel1" + field_str = fields.String() + + Datamodel1._build_datamodel(self.datamodel_registry) + for _i in range(0, 1000): + self.env.datamodels["datamodel1"](field_str="1234") + def test_nested_model(self): """ Test nested model serialization/deserialization""" From 44517efca6ad0497734ee81d32f9f15dd9db9b2d Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 23 Feb 2021 09:10:00 +0100 Subject: [PATCH 15/37] [FIX] datamodel: Avoid maximum recursion Into the datamodel constructor, we 'patch' the datamodel constructor to ensure the environment is propagated when a instance is created. Before this change, each time the factory was called, the init method was patched again. After some times, the maximum recusion limit was exceed. With this change we ensure that the patch is applied only once. --- datamodel/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/datamodel/core.py b/datamodel/core.py index 8d0575df8..2665341f2 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -378,6 +378,9 @@ def __init__(self, env, registry): def __getitem__(self, key): model = self.registry[key] + if hasattr(model, "__datamodel_init_patched"): + return model + model.__init__ = functools.partialmethod(model.__init__, env=self.env) @classmethod @@ -387,6 +390,7 @@ def __get_schema_class__(cls, **kwargs): return cls model.__get_schema_class__ = __get_schema_class__ + setattr(model, "__datamodel_init_patched", True) # noqa: B010 return model From a7a495bd858063de7d4aa721d92b2b623391b8f8 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Fri, 9 Jul 2021 10:13:58 +0200 Subject: [PATCH 16/37] [IMP] datamodels: Add readme --- datamodel/README.rst | 178 ++++++--- datamodel/readme/CONTRIBUTORS.rst | 1 + datamodel/readme/DESCRIPTION.rst | 5 + datamodel/readme/ROADMAP.rst | 3 + datamodel/readme/USAGE.rst | 76 ++++ datamodel/static/description/index.html | 503 ++++++++++++++++++++++++ 6 files changed, 720 insertions(+), 46 deletions(-) create mode 100644 datamodel/readme/CONTRIBUTORS.rst create mode 100644 datamodel/readme/DESCRIPTION.rst create mode 100644 datamodel/readme/ROADMAP.rst create mode 100644 datamodel/readme/USAGE.rst create mode 100644 datamodel/static/description/index.html diff --git a/datamodel/README.rst b/datamodel/README.rst index 1dda13fb9..ec7183226 100644 --- a/datamodel/README.rst +++ b/datamodel/README.rst @@ -1,86 +1,172 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 - ========= Datamodel ========= +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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%2Frest--framework-lightgray.png?logo=github + :target: https://github.com/OCA/rest-framework/tree/14.0/datamodel + :alt: OCA/rest-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rest-framework-14-0/rest-framework-14-0-datamodel + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/271/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + This addon allows you to define simple data models supporting serialization/deserialization +to/from json -Installation -============ +Datamodels are `Marshmallow models `_ classes that can be inherited as Odoo +Models. -To install this module, you need to: +**Table of contents** -#. Do this ... +.. contents:: + :local: -Configuration -============= +Usage +===== -To configure this module, you need to: +To define your own datamodel you just need to create a class that inherits from +``odoo.addons.datamodel.core.Datamodel`` -#. Go to ... +.. code-block:: python -.. figure:: path/to/local/image.png - :alt: alternative description - :width: 600 px + from marshmallow import fields -Usage -===== + from odoo.addons.base_rest import restapi + from odoo.addons.component.core import Component + from odoo.addons.datamodel.core import Datamodel + + + class PartnerShortInfo(Datamodel): + _name = "partner.short.info" + + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + + class PartnerInfo(Datamodel): + _name = "partner.info" + _inherit = "partner.short.info" + + street = fields.String(required=True, allow_none=False) + street2 = fields.String(required=False, allow_none=True) + zip_code = fields.String(required=True, allow_none=False) + city = fields.String(required=True, allow_none=False) + phone = fields.String(required=False, allow_none=True) + is_componay = fields.Boolean(required=False, allow_none=False) + + +As for odoo models, you can extend the `base` datamodel by inheriting of `base`. + +.. code-block:: python + + class Base(Datamodel): + _inherit = "base" + + def _my_method(self): + pass + +Datamodels are available through the `datamodels` registry provided by the Odoo's environment. -To use this module, you need to: +.. code-block:: python -#. Go to ... + class ResPartner(Model): + _inherit = "res.partner" -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/{repo_id}/{branch} + def _to_partner_info(self): + PartnerInfo = self.env.datamodels["partner.info"] + partner_info = PartnerInfo(partial=True) + partner_info.id = partner.id + partner_info.name = partner.name + partner_info.street = partner.street + partner_info.street2 = partner.street2 + partner_info.zip_code = partner.zip + partner_info.city = partner.city + partner_info.phone = partner.phone + partner_info.is_company = partner.is_company + return partner_info -.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt -.. branch is "8.0" for example +The Odoo's environment is also available into the datamodel instance. + +.. code-block:: python + + class MyDataModel(Datamodel): + _name = "my.data.model" + + def _my_method(self): + partners = self.env["res.partner"].search([]) + +.. warning:: + + The `env` property into a Datamodel instance is mutable. IOW, you can't rely + on information (context, user) provided by the environment. The `env` property + is a helper property that give you access to the odoo's registry and must + be use with caution. + +Known issues / Roadmap +====================== + +The `roadmap `_ +and `known issues `_ can +be found on GitHub. 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 smash it by providing detailed and welcomed feedback. +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 ======= -Images ------- +Authors +~~~~~~~ -* Odoo Community Association: `Icon `_. +* ACSONE SA/NV Contributors ------------- - -* Firstname Lastname -* Second Person +~~~~~~~~~~~~ -Funders -------- +* Laurent Mignon -The development of this module has been financially supported by: +Maintainers +~~~~~~~~~~~ -* Company 1 name -* Company 2 name - -Maintainer ----------- +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org -This module is maintained by the OCA. - 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. -To contribute to this module, please visit https://odoo-community.org. +.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px + :target: https://github.com/lmignon + :alt: lmignon + +Current `maintainer `__: + +|maintainer-lmignon| + +This module is part of the `OCA/rest-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/datamodel/readme/CONTRIBUTORS.rst b/datamodel/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..172b2d223 --- /dev/null +++ b/datamodel/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Laurent Mignon diff --git a/datamodel/readme/DESCRIPTION.rst b/datamodel/readme/DESCRIPTION.rst new file mode 100644 index 000000000..ea77e3c4d --- /dev/null +++ b/datamodel/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This addon allows you to define simple data models supporting serialization/deserialization +to/from json + +Datamodels are `Marshmallow models `_ classes that can be inherited as Odoo +Models. diff --git a/datamodel/readme/ROADMAP.rst b/datamodel/readme/ROADMAP.rst new file mode 100644 index 000000000..b2f5695f7 --- /dev/null +++ b/datamodel/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +The `roadmap `_ +and `known issues `_ can +be found on GitHub. diff --git a/datamodel/readme/USAGE.rst b/datamodel/readme/USAGE.rst new file mode 100644 index 000000000..8ee8fa2ee --- /dev/null +++ b/datamodel/readme/USAGE.rst @@ -0,0 +1,76 @@ +To define your own datamodel you just need to create a class that inherits from +``odoo.addons.datamodel.core.Datamodel`` + +.. code-block:: python + + from marshmallow import fields + + from odoo.addons.base_rest import restapi + from odoo.addons.component.core import Component + from odoo.addons.datamodel.core import Datamodel + + + class PartnerShortInfo(Datamodel): + _name = "partner.short.info" + + id = fields.Integer(required=True, allow_none=False) + name = fields.String(required=True, allow_none=False) + + class PartnerInfo(Datamodel): + _name = "partner.info" + _inherit = "partner.short.info" + + street = fields.String(required=True, allow_none=False) + street2 = fields.String(required=False, allow_none=True) + zip_code = fields.String(required=True, allow_none=False) + city = fields.String(required=True, allow_none=False) + phone = fields.String(required=False, allow_none=True) + is_componay = fields.Boolean(required=False, allow_none=False) + + +As for odoo models, you can extend the `base` datamodel by inheriting of `base`. + +.. code-block:: python + + class Base(Datamodel): + _inherit = "base" + + def _my_method(self): + pass + +Datamodels are available through the `datamodels` registry provided by the Odoo's environment. + +.. code-block:: python + + class ResPartner(Model): + _inherit = "res.partner" + + def _to_partner_info(self): + PartnerInfo = self.env.datamodels["partner.info"] + partner_info = PartnerInfo(partial=True) + partner_info.id = partner.id + partner_info.name = partner.name + partner_info.street = partner.street + partner_info.street2 = partner.street2 + partner_info.zip_code = partner.zip + partner_info.city = partner.city + partner_info.phone = partner.phone + partner_info.is_company = partner.is_company + return partner_info + +The Odoo's environment is also available into the datamodel instance. + +.. code-block:: python + + class MyDataModel(Datamodel): + _name = "my.data.model" + + def _my_method(self): + partners = self.env["res.partner"].search([]) + +.. warning:: + + The `env` property into a Datamodel instance is mutable. IOW, you can't rely + on information (context, user) provided by the environment. The `env` property + is a helper property that give you access to the odoo's registry and must + be use with caution. diff --git a/datamodel/static/description/index.html b/datamodel/static/description/index.html new file mode 100644 index 000000000..ead168e64 --- /dev/null +++ b/datamodel/static/description/index.html @@ -0,0 +1,503 @@ + + + + + + +Datamodel + + + +
+

Datamodel

+ + +

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runbot

+

This addon allows you to define simple data models supporting serialization/deserialization +to/from json

+

Datamodels are Marshmallow models classes that can be inherited as Odoo +Models.

+

Table of contents

+ +
+

Usage

+

To define your own datamodel you just need to create a class that inherits from +odoo.addons.datamodel.core.Datamodel

+
+from marshmallow import fields
+
+from odoo.addons.base_rest import restapi
+from odoo.addons.component.core import Component
+from odoo.addons.datamodel.core import Datamodel
+
+
+class PartnerShortInfo(Datamodel):
+    _name = "partner.short.info"
+
+    id = fields.Integer(required=True, allow_none=False)
+    name = fields.String(required=True, allow_none=False)
+
+class PartnerInfo(Datamodel):
+    _name = "partner.info"
+    _inherit = "partner.short.info"
+
+    street = fields.String(required=True, allow_none=False)
+    street2 = fields.String(required=False, allow_none=True)
+    zip_code = fields.String(required=True, allow_none=False)
+    city = fields.String(required=True, allow_none=False)
+    phone = fields.String(required=False, allow_none=True)
+    is_componay = fields.Boolean(required=False, allow_none=False)
+
+

As for odoo models, you can extend the base datamodel by inheriting of base.

+
+class Base(Datamodel):
+    _inherit = "base"
+
+    def _my_method(self):
+        pass
+
+

Datamodels are available through the datamodels registry provided by the Odoo’s environment.

+
+class ResPartner(Model):
+    _inherit = "res.partner"
+
+    def _to_partner_info(self):
+        PartnerInfo = self.env.datamodels["partner.info"]
+        partner_info = PartnerInfo(partial=True)
+        partner_info.id = partner.id
+        partner_info.name = partner.name
+        partner_info.street = partner.street
+        partner_info.street2 = partner.street2
+        partner_info.zip_code = partner.zip
+        partner_info.city = partner.city
+        partner_info.phone = partner.phone
+        partner_info.is_company = partner.is_company
+        return partner_info
+
+

The Odoo’s environment is also available into the datamodel instance.

+
+class MyDataModel(Datamodel):
+    _name = "my.data.model"
+
+    def _my_method(self):
+        partners = self.env["res.partner"].search([])
+
+
+

Warning

+

The env property into a Datamodel instance is mutable. IOW, you can’t rely +on information (context, user) provided by the environment. The env property +is a helper property that give you access to the odoo’s registry and must +be use with caution.

+
+
+
+

Known issues / Roadmap

+

The roadmap +and known issues can +be found on GitHub.

+
+
+

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

+
    +
  • ACSONE SA/NV
  • +
+
+
+

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/rest-framework project on GitHub.

+

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

+
+
+
+ + From 140cd9f60905581bb59a539e1661ac34487ece7d Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 9 Jul 2021 08:47:24 +0000 Subject: [PATCH 17/37] datamodel 14.0.3.0.1 --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 1e5cb38cd..03f002a00 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "14.0.3.0.0", + "version": "14.0.3.0.1", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", From 6a9fad319ea7ad29d0a7b457707c032c136668cc Mon Sep 17 00:00:00 2001 From: Carlos Roca Date: Mon, 12 Jul 2021 08:23:08 +0200 Subject: [PATCH 18/37] [FIX] datamodel: Error when NestedModel inside other field --- datamodel/fields.py | 12 ++++++-- datamodel/tests/test_build_datamodel.py | 38 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/datamodel/fields.py b/datamodel/fields.py index a2e52f006..ae3e65ad2 100644 --- a/datamodel/fields.py +++ b/datamodel/fields.py @@ -30,10 +30,18 @@ def __init__(self, nested, **kwargs): @property def schema(self): if not self.nested: - self.nested = self.parent._env.datamodels[ + # Get the major parent to avoid error of _env does not exist + super_parent = None + parent = self + while not super_parent: + if not hasattr(parent, "parent"): + super_parent = parent + break + parent = parent.parent + self.nested = super_parent._env.datamodels[ self.datamodel_name ].__schema_class__ - self.nested._env = self.parent._env + self.nested._env = super_parent._env return super(NestedModel, self).schema def _deserialize(self, value, attr, data, **kwargs): diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index 616215cb0..3b96985b4 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -365,6 +365,44 @@ class Child(Datamodel): self.assertEqual(new_instance.name, instance.name) self.assertEqual(new_instance.child.field_str, instance.child.field_str) + def test_list_nested_model(self): + """ Test list model of nested model serialization/deserialization""" + + class Parent(Datamodel): + _name = "parent" + name = fields.String() + list_child = fields.List(fields.NestedModel("child")) + + class Child(Datamodel): + _name = "child" + field_str = fields.String() + + Parent._build_datamodel(self.datamodel_registry) + Child._build_datamodel(self.datamodel_registry) + + Parent = self.env.datamodels["parent"] + Child = self.env.datamodels["child"] + + childs = [ + Child(field_str="My 1st other string"), + Child(field_str="My 2nd other string"), + ] + instance = Parent(name="Parent", list_child=childs) + res = instance.dump() + self.assertDictEqual( + res, + { + "list_child": [ + {"field_str": "My 1st other string"}, + {"field_str": "My 2nd other string"}, + ], + "name": "Parent", + }, + ) + new_instance = instance.load(res) + self.assertEqual(new_instance.name, instance.name) + self.assertEqual(new_instance.list_child, instance.list_child) + def test_many(self): """Test loads of many""" From b1c4022e3f1d48bfff20da0a6284c8cc8a1d98ee Mon Sep 17 00:00:00 2001 From: Carlos Roca Date: Mon, 12 Jul 2021 08:24:05 +0200 Subject: [PATCH 19/37] [IMP] datamodel: Add new readme --- datamodel/README.rst | 3 +++ datamodel/i18n/datamodel.pot | 34 +++++++++++++++++++++++++ datamodel/readme/CONTRIBUTORS.rst | 3 +++ datamodel/static/description/index.html | 10 +++++++- 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 datamodel/i18n/datamodel.pot diff --git a/datamodel/README.rst b/datamodel/README.rst index ec7183226..cbb7bd357 100644 --- a/datamodel/README.rst +++ b/datamodel/README.rst @@ -145,6 +145,9 @@ Contributors ~~~~~~~~~~~~ * Laurent Mignon +* `Tecnativa `_: + + * Carlos Roca Maintainers ~~~~~~~~~~~ diff --git a/datamodel/i18n/datamodel.pot b/datamodel/i18n/datamodel.pot new file mode 100644 index 000000000..863ff8e72 --- /dev/null +++ b/datamodel/i18n/datamodel.pot @@ -0,0 +1,34 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * datamodel +# +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: datamodel +#: model:ir.model,name:datamodel.model_datamodel_builder +msgid "Datamodel Builder" +msgstr "" + +#. module: datamodel +#: model:ir.model.fields,field_description:datamodel.field_datamodel_builder__display_name +msgid "Display Name" +msgstr "" + +#. module: datamodel +#: model:ir.model.fields,field_description:datamodel.field_datamodel_builder__id +msgid "ID" +msgstr "" + +#. module: datamodel +#: model:ir.model.fields,field_description:datamodel.field_datamodel_builder____last_update +msgid "Last Modified on" +msgstr "" diff --git a/datamodel/readme/CONTRIBUTORS.rst b/datamodel/readme/CONTRIBUTORS.rst index 172b2d223..b4b650de6 100644 --- a/datamodel/readme/CONTRIBUTORS.rst +++ b/datamodel/readme/CONTRIBUTORS.rst @@ -1 +1,4 @@ * Laurent Mignon +* `Tecnativa `_: + + * Carlos Roca diff --git a/datamodel/static/description/index.html b/datamodel/static/description/index.html index ead168e64..2f02b363c 100644 --- a/datamodel/static/description/index.html +++ b/datamodel/static/description/index.html @@ -481,8 +481,16 @@

Authors

Contributors

+
From 4134d28ec997dca6a2a21f708a5d0770ffa1e2cc Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 25 Aug 2021 07:59:03 +0000 Subject: [PATCH 20/37] datamodel 14.0.3.0.2 --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 03f002a00..e07903887 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "14.0.3.0.1", + "version": "14.0.3.0.2", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", From d160b0667f284a6d8900cd5f7ba7ccac1e058812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Degrave?= Date: Thu, 2 Sep 2021 15:53:13 +0200 Subject: [PATCH 21/37] [14.0][FIX] base_rest_datamodel: modify validation of responses to allow `dump_only` fields Forward port of PR https://github.com/OCA/rest-framework/pull/169/files --- datamodel/core.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/datamodel/core.py b/datamodel/core.py index 2665341f2..ee6c5a25e 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -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 @@ -41,6 +44,14 @@ 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 """ @@ -131,6 +142,13 @@ 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 @@ -207,6 +225,18 @@ def get_schema(cls, **kwargs): """ return cls.__get_schema_class__(**kwargs) + @classmethod + def validate(cls, data, context=None, many=None, partial=None, unknown=None): + schema = cls.__get_schema_class__( + context=context, partial=partial, unknown=unknown + ) + all_schemas = _get_nested_schemas(schema) + with ExitStack() as stack: + # 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. From a59d6be5d66a8dcf3b97b5fc655bef738e14d398 Mon Sep 17 00:00:00 2001 From: David James Date: Sun, 3 Oct 2021 19:04:26 +1100 Subject: [PATCH 22/37] datamodel: replace deprecated use of "missing" argument --- datamodel/tests/test_build_datamodel.py | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index 3b96985b4..f36cb8fdc 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -71,15 +71,15 @@ def test_inherit_bases(self): class Datamodel1(Datamodel): _name = "datamodel1" - field_str1 = fields.String(missing="field_str1") + field_str1 = fields.String(load_default="field_str1") class Datamodel2(Datamodel): _inherit = "datamodel1" - field_str2 = fields.String(missing="field_str2") + field_str2 = fields.String(load_default="field_str2") class Datamodel3(Datamodel): _inherit = "datamodel1" - field_str3 = fields.String(missing="field_str3") + field_str3 = fields.String(load_default="field_str3") Datamodel1._build_datamodel(self.datamodel_registry) Datamodel2._build_datamodel(self.datamodel_registry) @@ -94,18 +94,18 @@ def test_prototype_inherit_bases(self): class Datamodel1(Datamodel): _name = "datamodel1" - field_int = fields.Int(missing=1) + field_int = fields.Int(load_default=1) class Datamodel2(Datamodel): _name = "datamodel2" _inherit = "datamodel1" - field_boolean = fields.Boolean(missing=True) - field_int = fields.Int(missing=2) + field_boolean = fields.Boolean(load_default=True) + field_int = fields.Int(load_default=2) class Datamodel3(Datamodel): _name = "datamodel3" _inherit = "datamodel1" - field_float = fields.Float(missing=0.3) + field_float = fields.Float(load_default=0.3) class Datamodel4(Datamodel): _name = "datamodel4" @@ -150,18 +150,18 @@ def test_final_class_schema(self): class Datamodel1(Datamodel): _name = "datamodel1" - field_int = fields.Int(missing=1) + field_int = fields.Int(load_default=1) class Datamodel2(Datamodel): _name = "datamodel2" _inherit = "datamodel1" - field_boolean = fields.Boolean(missing=True) - field_int = fields.Int(missing=2) + field_boolean = fields.Boolean(load_default=True) + field_int = fields.Int(load_default=2) class Datamodel3(Datamodel): _name = "datamodel3" _inherit = "datamodel1" - field_float = fields.Float(missing=0.3) + field_float = fields.Float(load_default=0.3) class Datamodel4(Datamodel): _name = "datamodel4" @@ -308,16 +308,16 @@ def test_add_inheritance_final_schema(self): class Datamodel1(Datamodel): _name = "datamodel1" - field_str1 = fields.String(missing="str1") + field_str1 = fields.String(load_default="str1") class Datamodel2(Datamodel): _name = "datamodel2" - field_str2 = fields.String(missing="str2") + field_str2 = fields.String(load_default="str2") class Datamodel2bis(Datamodel): _name = "datamodel2" _inherit = ["datamodel2", "datamodel1"] - field_str3 = fields.String(missing="str3") + field_str3 = fields.String(load_default="str3") Datamodel1._build_datamodel(self.datamodel_registry) Datamodel2._build_datamodel(self.datamodel_registry) From 7120c34833628628efa17bf18b54c7b9fab7a89a Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 8 Oct 2021 07:06:11 +0000 Subject: [PATCH 23/37] datamodel 14.0.3.0.3 --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index e07903887..91ddf3e8f 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "14.0.3.0.2", + "version": "14.0.3.0.3", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", From b6aaf7f4f63e4d6ade37c34d0a4ef66c376002e5 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 8 Oct 2021 07:31:05 +0000 Subject: [PATCH 24/37] datamodel 14.0.3.0.4 --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 91ddf3e8f..a9e2b893f 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "14.0.3.0.3", + "version": "14.0.3.0.4", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", From da6a1309ab0b28ac750659d0e9ec310690ea6f39 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Mon, 22 Nov 2021 17:43:38 +0100 Subject: [PATCH 25/37] Initialize 15.0 branch --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index a9e2b893f..dbaf5ebcb 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -16,5 +16,5 @@ "data": [], "demo": [], "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, - "installable": True, + "installable": False, } From cc602495d8f4a7d074194519c3cecb9732034343 Mon Sep 17 00:00:00 2001 From: Maksym Yankin Date: Tue, 23 Nov 2021 15:56:39 +0200 Subject: [PATCH 26/37] [MIG] datamodel: Migration to 15.0 [UPD] rest-framework conf to include datamodel --- datamodel/README.rst | 10 ++-- datamodel/__manifest__.py | 4 +- datamodel/core.py | 4 +- datamodel/i18n/datamodel.pot | 17 +----- datamodel/static/description/index.html | 6 +-- datamodel/tests/common.py | 70 ++++++------------------- datamodel/tests/test_build_datamodel.py | 26 ++++----- 7 files changed, 43 insertions(+), 94 deletions(-) diff --git a/datamodel/README.rst b/datamodel/README.rst index cbb7bd357..53c753370 100644 --- a/datamodel/README.rst +++ b/datamodel/README.rst @@ -14,13 +14,13 @@ Datamodel :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github - :target: https://github.com/OCA/rest-framework/tree/14.0/datamodel + :target: https://github.com/OCA/rest-framework/tree/15.0/datamodel :alt: OCA/rest-framework .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/rest-framework-14-0/rest-framework-14-0-datamodel + :target: https://translation.odoo-community.org/projects/rest-framework-15-0/rest-framework-15-0-datamodel :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/271/14.0 + :target: https://runbot.odoo-community.org/runbot/271/15.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -129,7 +129,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 -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -170,6 +170,6 @@ Current `maintainer `__: |maintainer-lmignon| -This module is part of the `OCA/rest-framework `_ project on GitHub. +This module is part of the `OCA/rest-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index dbaf5ebcb..dcb81f855 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "14.0.3.0.4", + "version": "15.0.1.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", @@ -16,5 +16,5 @@ "data": [], "demo": [], "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, - "installable": False, + "installable": True, } diff --git a/datamodel/core.py b/datamodel/core.py index ee6c5a25e..a6770e424 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -53,7 +53,7 @@ def _get_nested_schemas(schema): class DatamodelDatabases(dict): - """ Holds a registry of datamodels for each database """ + """Holds a registry of datamodels for each database""" class DatamodelRegistry(object): @@ -213,7 +213,7 @@ def __init__(self, context=None, partial=None, env=None, **kwargs): @property def env(self): - """ Current datamodels registry""" + """Current datamodels registry""" return self._env @classmethod diff --git a/datamodel/i18n/datamodel.pot b/datamodel/i18n/datamodel.pot index 863ff8e72..4a57c9791 100644 --- a/datamodel/i18n/datamodel.pot +++ b/datamodel/i18n/datamodel.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 14.0\n" +"Project-Id-Version: Odoo Server 15.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" @@ -17,18 +17,3 @@ msgstr "" #: model:ir.model,name:datamodel.model_datamodel_builder msgid "Datamodel Builder" msgstr "" - -#. module: datamodel -#: model:ir.model.fields,field_description:datamodel.field_datamodel_builder__display_name -msgid "Display Name" -msgstr "" - -#. module: datamodel -#: model:ir.model.fields,field_description:datamodel.field_datamodel_builder__id -msgid "ID" -msgstr "" - -#. module: datamodel -#: model:ir.model.fields,field_description:datamodel.field_datamodel_builder____last_update -msgid "Last Modified on" -msgstr "" diff --git a/datamodel/static/description/index.html b/datamodel/static/description/index.html index 2f02b363c..389080ad9 100644 --- a/datamodel/static/description/index.html +++ b/datamodel/static/description/index.html @@ -367,7 +367,7 @@

Datamodel

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runbot

+

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runbot

This addon allows you to define simple data models supporting serialization/deserialization to/from json

Datamodels are Marshmallow models classes that can be inherited as Odoo @@ -468,7 +468,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 -feedback.

+feedback.

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

@@ -502,7 +502,7 @@

Maintainers

promote its widespread use.

Current maintainer:

lmignon

-

This module is part of the OCA/rest-framework project on GitHub.

+

This module is part of the OCA/rest-framework project on GitHub.

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

diff --git a/datamodel/tests/common.py b/datamodel/tests/common.py index 1eb530654..9594ff3da 100644 --- a/datamodel/tests/common.py +++ b/datamodel/tests/common.py @@ -60,7 +60,7 @@ def notready(): class TransactionDatamodelCase(common.TransactionCase, DatamodelMixin): """A TransactionCase that loads all the datamodels - It it used like an usual Odoo's TransactionCase, but it ensures + It is used like an usual Odoo's TransactionCase, but it ensures that all the datamodels of the current addon and its dependencies are loaded. @@ -68,7 +68,7 @@ class TransactionDatamodelCase(common.TransactionCase, DatamodelMixin): @classmethod def setUpClass(cls): - super(TransactionDatamodelCase, cls).setUpClass() + super().setUpClass() cls.setUpDatamodel() # pylint: disable=W8106 @@ -79,30 +79,8 @@ def setUp(self): DatamodelMixin.setUp(self) -class SavepointDatamodelCase(common.SavepointCase, DatamodelMixin): - """A SavepointCase that loads all the datamodels - - It is used like an usual Odoo's SavepointCase, but it ensures - that all the datamodels of the current addon and its dependencies - are loaded. - - """ - - @classmethod - def setUpClass(cls): - super(SavepointDatamodelCase, cls).setUpClass() - cls.setUpDatamodel() - - # pylint: disable=W8106 - def setUp(self): - # resolve an inheritance issue (common.SavepointCase does not call - # super) - common.SavepointCase.setUp(self) - DatamodelMixin.setUp(self) - - class DatamodelRegistryCase( - common.TreeCase, common.MetaCase("DummyCase", (object,), {}) + common.BaseCase, common.MetaCase("DummyCase", (object,), {}) ): """This test case can be used as a base for writings tests on datamodels @@ -111,10 +89,9 @@ class DatamodelRegistryCase( or not, or when you want to create additional datamodels in your tests. If you only want to *use* the datamodels of the tested addon in your tests, - then consider using one of: + then consider using: * :class:`TransactionDatamodelCase` - * :class:`SavepointDatamodelCase` This test case creates a special :class:`odoo.addons.datamodel.core.DatamodelRegistry` for the purpose of @@ -136,8 +113,7 @@ class DatamodelRegistryCase( """ def setUp(self): - super(DatamodelRegistryCase, self).setUp() - + super().setUp() # keep the original classes registered by the metaclass # so we'll restore them at the end of the tests, it avoid # to pollute it with Stub / Test datamodels @@ -179,7 +155,7 @@ def _close_and_roolback(): self.datamodel_registry.ready = True def tearDown(self): - super(DatamodelRegistryCase, self).tearDown() + super().tearDown() # restore the original metaclass' classes MetaDatamodel._modules_datamodels = self._original_datamodels @@ -192,32 +168,18 @@ def _build_datamodels(self, *classes): class TransactionDatamodelRegistryCase(common.TransactionCase, DatamodelRegistryCase): - """ Adds Odoo Transaction in the base Datamodel TestCase """ + """Adds Odoo Transaction in the base Datamodel TestCase""" # pylint: disable=W8106 - def setUp(self): + @classmethod + def setUpClass(cls): # resolve an inheritance issue (common.TransactionCase does not use # super) - common.TransactionCase.setUp(self) - DatamodelRegistryCase.setUp(self) - self.collection = self.env["collection.base"] - - def teardown(self): - common.TransactionCase.tearDown(self) - DatamodelRegistryCase.tearDown(self) - - -class SavepointDatamodelRegistryCase(common.SavepointCase, DatamodelRegistryCase): - """ Adds Odoo Transaction with Savepoint in the base Datamodel TestCase """ - - # pylint: disable=W8106 - def setUp(self): - # resolve an inheritance issue (common.SavepointCase does not use - # super) - common.SavepointCase.setUp(self) - DatamodelRegistryCase.setUp(self) - self.collection = self.env["collection.base"] + common.TransactionCase.setUpClass(cls) + DatamodelRegistryCase.setUp(cls) + cls.collection = cls.env["collection.base"] - def teardown(self): - common.SavepointCase.tearDown(self) - DatamodelRegistryCase.tearDown(self) + @classmethod + def tearDownClass(cls): + common.TransactionCase.tearDownClass(cls) + DatamodelRegistryCase.tearDown(cls) diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index f36cb8fdc..b0ff97f06 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -40,7 +40,7 @@ class Datamodel2(Datamodel): self.assertIsInstance(Datamodel2(), MarshmallowModel) def test_no_name(self): - """ Ensure that a datamodel has a _name """ + """Ensure that a datamodel has a _name""" class Datamodel1(Datamodel): pass @@ -50,7 +50,7 @@ class Datamodel1(Datamodel): Datamodel1._build_datamodel(self.datamodel_registry) def test_register(self): - """ Able to register datamodels in datamodels registry """ + """Able to register datamodels in datamodels registry""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -66,8 +66,9 @@ class Datamodel2(Datamodel): ["base", "datamodel1", "datamodel2"], list(self.datamodel_registry) ) + # pylint: disable=R7980 def test_inherit_bases(self): - """ Check __bases__ of Datamodel with _inherit """ + """Check __bases__ of Datamodel with _inherit""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -90,7 +91,7 @@ class Datamodel3(Datamodel): ) def test_prototype_inherit_bases(self): - """ Check __bases__ of Datamodel with _inherit and different _name """ + """Check __bases__ of Datamodel with _inherit and different _name""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -188,7 +189,7 @@ class Datamodel4(Datamodel): ) def test_custom_build(self): - """ Check that we can hook at the end of a Datamodel build """ + """Check that we can hook at the end of a Datamodel build""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -203,8 +204,9 @@ def _complete_datamodel_build(cls): # we inspect that our custom build has been executed self.assertTrue(self.env.datamodels["datamodel1"]._build_done) + # pylint: disable=W8110 def test_inherit_attrs(self): - """ Check attributes inheritance of Datamodels with _inherit """ + """Check attributes inheritance of Datamodels with _inherit""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -236,7 +238,7 @@ def say(self): self.assertEqual("foo bar", datamodel2.say()) def test_duplicate_datamodel(self): - """ Check that we can't have 2 datamodels with the same name """ + """Check that we can't have 2 datamodels with the same name""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -250,7 +252,7 @@ class Datamodel2(Datamodel): Datamodel2._build_datamodel(self.datamodel_registry) def test_no_parent(self): - """ Ensure we can't _inherit a non-existent datamodel """ + """Ensure we can't _inherit a non-existent datamodel""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -261,7 +263,7 @@ class Datamodel1(Datamodel): Datamodel1._build_datamodel(self.datamodel_registry) def test_no_parent2(self): - """ Ensure we can't _inherit by prototype a non-existent datamodel """ + """Ensure we can't _inherit by prototype a non-existent datamodel""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -276,7 +278,7 @@ class Datamodel2(Datamodel): Datamodel2._build_datamodel(self.datamodel_registry) def test_add_inheritance(self): - """ Ensure we can add a new inheritance """ + """Ensure we can add a new inheritance""" class Datamodel1(Datamodel): _name = "datamodel1" @@ -339,7 +341,7 @@ class Datamodel1(Datamodel): self.env.datamodels["datamodel1"](field_str="1234") def test_nested_model(self): - """ Test nested model serialization/deserialization""" + """Test nested model serialization/deserialization""" class Parent(Datamodel): _name = "parent" @@ -366,7 +368,7 @@ class Child(Datamodel): self.assertEqual(new_instance.child.field_str, instance.child.field_str) def test_list_nested_model(self): - """ Test list model of nested model serialization/deserialization""" + """Test list model of nested model serialization/deserialization""" class Parent(Datamodel): _name = "parent" From 5cb43699f1d5be7ce0148610b82994907c815c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Degrave?= Date: Wed, 15 Dec 2021 17:07:02 +0100 Subject: [PATCH 27/37] [FIX] datamodel: env of datamodel instances is not refreshed (causing closed cursor exceptions) [FIX] datamodel: env of datamodel instances is not refreshed (causing closed cursor exceptions) --- datamodel/core.py | 10 ++-------- datamodel/tests/test_build_datamodel.py | 7 ++++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/datamodel/core.py b/datamodel/core.py index a6770e424..d8838ae27 100644 --- a/datamodel/core.py +++ b/datamodel/core.py @@ -2,7 +2,6 @@ # Copyright 2019 ACSONE SA/NV # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) -import functools import logging from collections import OrderedDict, defaultdict from contextlib import ExitStack @@ -20,7 +19,6 @@ except ImportError: _logger.debug("Cannot import 'marshmallow_objects'.") - # The Cache size represents the number of items, so the number # of datamodels (include abstract datamodels) we will keep in the LRU # cache. We would need stats to know what is the average but this is a bit @@ -208,7 +206,7 @@ class AnotherDatamodel(Datamodel): _inherit = None def __init__(self, context=None, partial=None, env=None, **kwargs): - self._env = env + self._env = env or type(self)._env super().__init__(context=context, partial=partial, **kwargs) @property @@ -408,10 +406,7 @@ def __init__(self, env, registry): def __getitem__(self, key): model = self.registry[key] - if hasattr(model, "__datamodel_init_patched"): - return model - - model.__init__ = functools.partialmethod(model.__init__, env=self.env) + model._env = self.env @classmethod def __get_schema_class__(cls, **kwargs): @@ -420,7 +415,6 @@ def __get_schema_class__(cls, **kwargs): return cls model.__get_schema_class__ = __get_schema_class__ - setattr(model, "__datamodel_init_patched", True) # noqa: B010 return model diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index b0ff97f06..fde04c0d2 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -5,6 +5,8 @@ import mock from marshmallow_objects.models import Model as MarshmallowModel +from odoo import SUPERUSER_ID, api + from .. import fields from ..core import Datamodel from .common import DatamodelRegistryCase, TransactionDatamodelCase @@ -299,7 +301,7 @@ class Datamodel2bis(Datamodel): Datamodel2bis, Datamodel2, self.env.datamodels["datamodel1"], - self.env.datamodels["base"], + self.env.datamodels.registry.get("base"), ), self.env.datamodels["datamodel2"].__bases__, ) @@ -471,6 +473,9 @@ class Item(Datamodel): self.assertEqual(instance.items[0].env, self.env) schema = instance.items[0].get_schema() self.assertEqual(schema._env, self.env) + another_env = api.Environment(self.env.registry.cursor(), SUPERUSER_ID, {}) + new_p = another_env.datamodels["parent"]() + self.assertEqual(new_p.env, another_env) class TestRegistryAccess(TransactionDatamodelCase): From 8d6600d200a58f82c2a42c666fb75b8acbe7e594 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 24 Dec 2021 13:10:03 +0000 Subject: [PATCH 28/37] datamodel 14.0.3.0.5 --- datamodel/__manifest__.py | 2 +- datamodel/tests/common.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index dcb81f855..24e1e8a1b 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "15.0.1.0.0", + "version": "14.0.3.0.5", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", diff --git a/datamodel/tests/common.py b/datamodel/tests/common.py index 9594ff3da..76c95c838 100644 --- a/datamodel/tests/common.py +++ b/datamodel/tests/common.py @@ -179,6 +179,22 @@ def setUpClass(cls): DatamodelRegistryCase.setUp(cls) cls.collection = cls.env["collection.base"] + def teardown(self): + common.TransactionCase.tearDown(self) + DatamodelRegistryCase.tearDown(self) + + +class SavepointDatamodelRegistryCase(common.SavepointCase, DatamodelRegistryCase): + """Adds Odoo Transaction with Savepoint in the base Datamodel TestCase""" + + # pylint: disable=W8106 + def setUp(self): + # resolve an inheritance issue (common.SavepointCase does not use + # super) + common.SavepointCase.setUp(self) + DatamodelRegistryCase.setUp(self) + self.collection = self.env["collection.base"] + @classmethod def tearDownClass(cls): common.TransactionCase.tearDownClass(cls) From e55e7d69c409fe38d4ec64c54c98e385e90410ac Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 14 Jun 2022 13:22:34 +0000 Subject: [PATCH 29/37] datamodel 15.0.1.0.1 --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 24e1e8a1b..902c7a3eb 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,7 +6,7 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "14.0.3.0.5", + "version": "15.0.1.0.1", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", From e677472266414a3254260b6a605358019abd14fe Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 27 Sep 2022 11:27:21 +0200 Subject: [PATCH 30/37] Initialize 16.0 branch --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 902c7a3eb..45a117b4c 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -16,5 +16,5 @@ "data": [], "demo": [], "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, - "installable": True, + "installable": False, } From ba3d161aebfc38651966b2a7112704d04fd22bcd Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Wed, 7 Dec 2022 17:05:27 +0100 Subject: [PATCH 31/37] [MIG] base_rest, base_rest_auth_api_key, base_rest_datamodel, base_rest_demo, base_rest_pydantic, datamodel, extendable: pre-commit stuff --- datamodel/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 45a117b4c..902c7a3eb 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -16,5 +16,5 @@ "data": [], "demo": [], "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, - "installable": False, + "installable": True, } From 2f9b507e553bac832b2847f51b5efa8023a87f26 Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Mon, 28 Nov 2022 18:50:51 +0530 Subject: [PATCH 32/37] [MIG] base_rest, base_rest_auth_api_key, base_rest_datamodel, base_rest_demo, base_rest_pydantic, datamodel, extendable: Migration to 16.0 Co-authored-by: Nikul-OSI --- datamodel/README.rst | 23 +++-- datamodel/__manifest__.py | 5 +- datamodel/i18n/datamodel.pot | 2 +- datamodel/static/description/index.html | 128 ++++++++++++------------ datamodel/tests/test_build_datamodel.py | 3 +- 5 files changed, 82 insertions(+), 79 deletions(-) diff --git a/datamodel/README.rst b/datamodel/README.rst index 53c753370..e8006e73c 100644 --- a/datamodel/README.rst +++ b/datamodel/README.rst @@ -2,10 +2,13 @@ Datamodel ========= -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2db0b5f2f951f3868beca32ecbeb8b14dff6da200ae1f21f8d279987ca46ca35 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -14,16 +17,16 @@ Datamodel :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github - :target: https://github.com/OCA/rest-framework/tree/15.0/datamodel + :target: https://github.com/OCA/rest-framework/tree/16.0/datamodel :alt: OCA/rest-framework .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/rest-framework-15-0/rest-framework-15-0-datamodel + :target: https://translation.odoo-community.org/projects/rest-framework-16-0/rest-framework-16-0-datamodel :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/271/15.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/rest-framework&target_branch=16.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|badge1| |badge2| |badge3| |badge4| |badge5| This addon allows you to define simple data models supporting serialization/deserialization to/from json @@ -128,8 +131,8 @@ 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 `_. +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. @@ -170,6 +173,6 @@ Current `maintainer `__: |maintainer-lmignon| -This module is part of the `OCA/rest-framework `_ project on GitHub. +This module is part of the `OCA/rest-framework `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/datamodel/__manifest__.py b/datamodel/__manifest__.py index 902c7a3eb..af37c113d 100644 --- a/datamodel/__manifest__.py +++ b/datamodel/__manifest__.py @@ -6,15 +6,12 @@ "summary": """ This addon allows you to define simple data models supporting serialization/deserialization""", - "version": "15.0.1.0.1", + "version": "16.0.1.0.0", "license": "LGPL-3", "development_status": "Beta", "author": "ACSONE SA/NV, " "Odoo Community Association (OCA)", "maintainers": ["lmignon"], "website": "https://github.com/OCA/rest-framework", - "depends": [], - "data": [], - "demo": [], "external_dependencies": {"python": ["marshmallow", "marshmallow-objects>=2.0.0"]}, "installable": True, } diff --git a/datamodel/i18n/datamodel.pot b/datamodel/i18n/datamodel.pot index 4a57c9791..970d66466 100644 --- a/datamodel/i18n/datamodel.pot +++ b/datamodel/i18n/datamodel.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 15.0\n" +"Project-Id-Version: Odoo Server 16.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: \n" "Language-Team: \n" diff --git a/datamodel/static/description/index.html b/datamodel/static/description/index.html index 389080ad9..16acef4ea 100644 --- a/datamodel/static/description/index.html +++ b/datamodel/static/description/index.html @@ -1,20 +1,20 @@ - + - + Datamodel -
-

Datamodel

+
+ + +Odoo Community Association + +
+

Datamodel

-

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

-

This addon allows you to define simple data models supporting serialization/deserialization -to/from json

-

Datamodels are Marshmallow models classes that can be inherited as Odoo -Models.

+

Beta License: LGPL-3 OCA/rest-framework Translate me on Weblate Try me on Runboat

+

This addon allows you to define simple data models supporting +serialization/deserialization to/from json

+

Datamodels are Marshmallow +models classes that +can be inherited as Odoo Models.

Table of contents

    @@ -389,24 +395,24 @@

    Datamodel

-

Usage

-

To define your own datamodel you just need to create a class that inherits from -odoo.addons.datamodel.core.Datamodel

+

Usage

+

To define your own datamodel you just need to create a class that +inherits from odoo.addons.datamodel.core.Datamodel

-from marshmallow import fields
+from marshmallow import fields
 
-from odoo.addons.base_rest import restapi
-from odoo.addons.component.core import Component
-from odoo.addons.datamodel.core import Datamodel
+from odoo.addons.base_rest import restapi
+from odoo.addons.component.core import Component
+from odoo.addons.datamodel.core import Datamodel
 
 
-class PartnerShortInfo(Datamodel):
+class PartnerShortInfo(Datamodel):
     _name = "partner.short.info"
 
     id = fields.Integer(required=True, allow_none=False)
     name = fields.String(required=True, allow_none=False)
 
-class PartnerInfo(Datamodel):
+class PartnerInfo(Datamodel):
     _name = "partner.info"
     _inherit = "partner.short.info"
 
@@ -417,20 +423,22 @@ 

Usage

phone = fields.String(required=False, allow_none=True) is_componay = fields.Boolean(required=False, allow_none=False)
-

As for odoo models, you can extend the base datamodel by inheriting of base.

+

As for odoo models, you can extend the base datamodel by inheriting of +base.

-class Base(Datamodel):
+class Base(Datamodel):
     _inherit = "base"
 
-    def _my_method(self):
+    def _my_method(self):
         pass
 
-

Datamodels are available through the datamodels registry provided by the Odoo’s environment.

+

Datamodels are available through the datamodels registry provided by the +Odoo’s environment.

-class ResPartner(Model):
+class ResPartner(Model):
     _inherit = "res.partner"
 
-    def _to_partner_info(self):
+    def _to_partner_info(self):
         PartnerInfo = self.env.datamodels["partner.info"]
         partner_info = PartnerInfo(partial=True)
         partner_info.id = partner.id
@@ -445,44 +453,46 @@ 

Usage

The Odoo’s environment is also available into the datamodel instance.

-class MyDataModel(Datamodel):
+class MyDataModel(Datamodel):
     _name = "my.data.model"
 
-    def _my_method(self):
+    def _my_method(self):
         partners = self.env["res.partner"].search([])
 

Warning

-

The env property into a Datamodel instance is mutable. IOW, you can’t rely -on information (context, user) provided by the environment. The env property -is a helper property that give you access to the odoo’s registry and must -be use with caution.

+

The env property into a Datamodel instance is mutable. IOW, you can’t +rely on information (context, user) provided by the environment. The +env property is a helper property that give you access to the odoo’s +registry and must be use with caution.

-

Known issues / Roadmap

-

The roadmap -and known issues can -be found on GitHub.

+

Known issues / Roadmap

+

The +roadmap +and known +issues +can be found on GitHub.

-

Bug Tracker

+

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 to smash it by providing a detailed and welcomed -feedback.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • ACSONE SA/NV
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +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.

Current maintainer:

lmignon

-

This module is part of the OCA/rest-framework project on GitHub.

+

This module is part of the OCA/rest-framework project on GitHub.

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

+
diff --git a/datamodel/tests/common.py b/datamodel/tests/common.py index 6c8843918..2afe3668b 100644 --- a/datamodel/tests/common.py +++ b/datamodel/tests/common.py @@ -7,6 +7,7 @@ import odoo from odoo import api +from odoo.modules.registry import Registry from odoo.tests import common from ..core import ( @@ -19,7 +20,7 @@ @contextmanager def new_rollbacked_env(): - registry = odoo.registry(common.get_db_name()) + registry = Registry(common.get_db_name()) uid = odoo.SUPERUSER_ID cr = registry.cursor() try: @@ -79,9 +80,7 @@ def setUp(self): DatamodelMixin.setUp(self) -class DatamodelRegistryCase( - common.BaseCase, common.MetaCase("DummyCase", (object,), {}) -): +class DatamodelRegistryCase(common.BaseCase): """This test case can be used as a base for writings tests on datamodels This test case is meant to test datamodels in a special datamodel registry, @@ -112,52 +111,60 @@ class DatamodelRegistryCase( need to explicitly pass ``self.datamodel_registry`` in the """ - def setUp(self): - super().setUp() + @classmethod + def setUpClass(cls): + super().setUpClass() # keep the original classes registered by the metaclass # so we'll restore them at the end of the tests, it avoid # to pollute it with Stub / Test datamodels - self._original_datamodels = copy.deepcopy(MetaDatamodel._modules_datamodels) + cls._original_datamodels = copy.deepcopy(MetaDatamodel._modules_datamodels) # it will be our temporary datamodel registry for our test session - self.datamodel_registry = DatamodelRegistry() + cls.datamodel_registry = DatamodelRegistry() # it builds the 'final datamodel' for every datamodel of the # 'datamodel' addon and push them in the datamodel registry - self.datamodel_registry.load_datamodels("datamodel") + cls.datamodel_registry.load_datamodels("datamodel") # build the datamodels of every installed addons already installed # but the current addon (when running with pytest/nosetest, we # simulate the --test-enable behavior by excluding the current addon # which is in 'to install' / 'to upgrade' with --test-enable). - current_addon = _get_addon_name(self.__module__) + current_addon = _get_addon_name(cls.__module__) - registry = odoo.registry(common.get_db_name()) - uid = odoo.SUPERUSER_ID + registry = Registry(common.get_db_name()) cr = registry.cursor() + uid = odoo.SUPERUSER_ID env = api.Environment(cr, uid, {}) env["datamodel.builder"].build_registry( - self.datamodel_registry, + cls.datamodel_registry, states=("installed",), exclude_addons=[current_addon], ) - self.env = env - _datamodel_databases[self.env.cr.dbname] = self.datamodel_registry + cls.env = env + _datamodel_databases[cls.env.cr.dbname] = cls.datamodel_registry - @self.addCleanup - def _close_and_roolback(): + def _close_and_rollback(): cr.rollback() # we shouldn't have to commit anything cr.close() + cls.addClassCleanup(_close_and_rollback) + # Fake that we are ready to work with the registry # normally, it is set to True and the end of the build # of the datamodels. Here, we'll add datamodels later in # the datamodels registry, but we don't mind for the tests. - self.datamodel_registry.ready = True + cls.datamodel_registry.ready = True + cls._original_registry_datamodels = copy.deepcopy( + cls.datamodel_registry._datamodels + ) def tearDown(self): super().tearDown() # restore the original metaclass' classes MetaDatamodel._modules_datamodels = self._original_datamodels + self.datamodel_registry._datamodels = copy.deepcopy( + self._original_registry_datamodels + ) def _load_module_datamodels(self, module): self.datamodel_registry.load_datamodels(module) @@ -184,18 +191,18 @@ def teardown(self): DatamodelRegistryCase.tearDown(self) -class SavepointDatamodelRegistryCase(common.SavepointCase, DatamodelRegistryCase): +class SavepointDatamodelRegistryCase(common.TransactionCase, DatamodelRegistryCase): """Adds Odoo Transaction with Savepoint in the base Datamodel TestCase""" - # pylint: disable=W8106 - def setUp(self): + @classmethod + def setUpClass(cls): # resolve an inheritance issue (common.SavepointCase does not use # super) - common.SavepointCase.setUp(self) - DatamodelRegistryCase.setUp(self) - self.collection = self.env["collection.base"] + super().setUpClass() + cls.collection = cls.env["collection.base"] @classmethod def tearDownClass(cls): + super().tearDownClass() common.TransactionCase.tearDownClass(cls) DatamodelRegistryCase.tearDown(cls) diff --git a/datamodel/tests/test_build_datamodel.py b/datamodel/tests/test_build_datamodel.py index d80c778df..ddc431e8b 100644 --- a/datamodel/tests/test_build_datamodel.py +++ b/datamodel/tests/test_build_datamodel.py @@ -69,7 +69,6 @@ class Datamodel2(Datamodel): ["base", "datamodel1", "datamodel2"], list(self.datamodel_registry) ) - # pylint: disable=R7980 def test_inherit_bases(self): """Check __bases__ of Datamodel with _inherit""" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..bae1b322b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# generated from manifests external_dependencies +marshmallow-objects>=2.0.0 +marshmallow<4.0.0