mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 08:31:59 +02:00
428 lines
15 KiB
Python
428 lines
15 KiB
Python
# Copyright 2017 Camptocamp SA
|
|
# Copyright 2019 ACSONE SA/NV
|
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
|
|
|
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
|
|
|
|
_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
|
|
|
|
|
|
def _get_nested_schemas(schema):
|
|
res = [schema]
|
|
for field in schema.fields.values():
|
|
if getattr(field, "schema", None):
|
|
res += _get_nested_schemas(field.schema)
|
|
return res
|
|
|
|
|
|
class DatamodelDatabases(dict):
|
|
"""Holds a registry of datamodels for each database"""
|
|
|
|
|
|
class DatamodelRegistry(object):
|
|
"""Store all the datamodel and allow to retrieve them by name
|
|
|
|
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._env.datamodels[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)
|
|
|
|
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
|
|
|
|
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.datamodel.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
|
|
_env = None # Odoo Environment
|
|
|
|
# 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, context=None, partial=None, env=None, **kwargs):
|
|
self._env = env or type(self)._env
|
|
super().__init__(context=context, partial=partial, **kwargs)
|
|
|
|
@property
|
|
def env(self):
|
|
return self._env
|
|
|
|
@classmethod
|
|
def get_schema(cls, **kwargs):
|
|
"""
|
|
Get a marshmallow schema instance
|
|
:param kwargs:
|
|
:return:
|
|
"""
|
|
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.
|
|
|
|
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.
|
|
"""
|
|
|
|
|
|
# makes the datamodels registry available on env
|
|
|
|
|
|
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._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
|
|
|
|
|
|
@property
|
|
def datamodels(self):
|
|
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
|