mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 06:11:59 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
428
odoo-bringout-oca-rest-framework-datamodel/datamodel/core.py
Normal file
428
odoo-bringout-oca-rest-framework-datamodel/datamodel/core.py
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue