858 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			858 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Python
		
	
	
| # -*- coding: utf-8 -*-
 | |
| # Copyright 2017 Camptocamp SA
 | |
| # Copyright 2017 Odoo
 | |
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
 | |
| 
 | |
| """
 | |
| 
 | |
| Core
 | |
| ====
 | |
| 
 | |
| Core classes for the components.
 | |
| The most common classes used publicly are:
 | |
| 
 | |
| * :class:`Component`
 | |
| * :class:`AbstractComponent`
 | |
| * :class:`WorkContext`
 | |
| 
 | |
| """
 | |
| 
 | |
| import logging
 | |
| import operator
 | |
| 
 | |
| from collections import defaultdict, OrderedDict
 | |
| 
 | |
| from odoo import models
 | |
| from odoo.tools import OrderedSet, LastOrderedSet
 | |
| from .exception import NoComponentError, SeveralComponentError
 | |
| 
 | |
| 
 | |
| _logger = logging.getLogger(__name__)
 | |
| 
 | |
| try:
 | |
|     from cachetools import LRUCache, cachedmethod
 | |
| except ImportError:
 | |
|     _logger.debug("Cannot import 'cachetools'.")
 | |
| 
 | |
| 
 | |
| # The Cache size represents the number of items, so the number
 | |
| # of components (include abstract components) 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 = full_name.split('.')[2]
 | |
|     else:
 | |
|         addon_name = full_name.split('.')[0]
 | |
|     return addon_name
 | |
| 
 | |
| 
 | |
| class ComponentDatabases(dict):
 | |
|     """ Holds a registry of components for each database """
 | |
| 
 | |
| 
 | |
| class ComponentRegistry(object):
 | |
|     """ Store all the components and allow to find them using criteria
 | |
| 
 | |
|     The key is the ``_name`` of the components.
 | |
| 
 | |
|     This is an OrderedDict, because we want to keep the registration order of
 | |
|     the components, addons loaded first have their components found first.
 | |
| 
 | |
|     The :attr:`ready` attribute must be set to ``True`` when all the components
 | |
|     are loaded.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     def __init__(self, cachesize=DEFAULT_CACHE_SIZE):
 | |
|         self._cache = LRUCache(maxsize=cachesize)
 | |
|         self._components = OrderedDict()
 | |
|         self._loaded_modules = set()
 | |
|         self.ready = False
 | |
| 
 | |
|     def __getitem__(self, key):
 | |
|         return self._components[key]
 | |
| 
 | |
|     def __setitem__(self, key, value):
 | |
|         self._components[key] = value
 | |
| 
 | |
|     def __contains__(self, key):
 | |
|         return key in self._components
 | |
| 
 | |
|     def get(self, key, default=None):
 | |
|         return self._components.get(key, default)
 | |
| 
 | |
|     def __iter__(self):
 | |
|         return iter(self._components)
 | |
| 
 | |
|     def load_components(self, module):
 | |
|         if module in self._loaded_modules:
 | |
|             return
 | |
|         for component_class in MetaComponent._modules_components[module]:
 | |
|             component_class._build_component(self)
 | |
|         self._loaded_modules.add(module)
 | |
| 
 | |
|     @cachedmethod(operator.attrgetter('_cache'))
 | |
|     def lookup(self, collection_name=None, usage=None, model_name=None):
 | |
|         """ Find and return a list of components for a usage
 | |
| 
 | |
|         If a component is not registered in a particular collection (no
 | |
|         ``_collection``), it might will be returned in any case (as far as
 | |
|         the ``usage`` and ``model_name`` match).  This is useful to share
 | |
|         generic components across different collections.
 | |
| 
 | |
|         If no collection name is given, components from any collection
 | |
|         will be returned.
 | |
| 
 | |
|         Then, the components of a collection are filtered by usage and/or
 | |
|         model. The ``_usage`` is mandatory on the components. When the
 | |
|         ``_model_name`` is empty, it means it can be used for every models,
 | |
|         and it will ignore the ``model_name`` argument.
 | |
| 
 | |
|         The abstract components are never returned.
 | |
| 
 | |
|         This is a rather low-level function, usually you will use the
 | |
|         high-level :meth:`AbstractComponent.component`,
 | |
|         :meth:`AbstractComponent.many_components` or even
 | |
|         :meth:`AbstractComponent.component_by_name`.
 | |
| 
 | |
|         :param collection_name: the name of the collection the component is
 | |
|                                 registered into.
 | |
|         :param usage: the usage of component we are looking for
 | |
|         :param model_name: filter on components that apply on this model
 | |
| 
 | |
|         """
 | |
| 
 | |
|         # keep the order so addons loaded first have components used first
 | |
|         candidates = (
 | |
|             component for component in self._components.values()
 | |
|             if not component._abstract
 | |
|         )
 | |
| 
 | |
|         if collection_name is not None:
 | |
|             candidates = (
 | |
|                 component for component in candidates
 | |
|                 if (component._collection == collection_name or
 | |
|                     component._collection is None)
 | |
|             )
 | |
| 
 | |
|         if usage is not None:
 | |
|             candidates = (component for component in candidates
 | |
|                           if component._usage == usage)
 | |
| 
 | |
|         if model_name is not None:
 | |
|             candidates = (c for c in candidates
 | |
|                           if c.apply_on_models is None or
 | |
|                           model_name in c.apply_on_models)
 | |
| 
 | |
|         return list(candidates)
 | |
| 
 | |
| 
 | |
| # We will store a ComponentRegistry per database here,
 | |
| # it will be cleared and updated when the odoo's registry is rebuilt
 | |
| _component_databases = ComponentDatabases()
 | |
| 
 | |
| 
 | |
| class WorkContext(object):
 | |
|     """ Transport the context required to work with components
 | |
| 
 | |
|     It is propagated through all the components, so any
 | |
|     data or instance (like a random RPC client) that need
 | |
|     to be propagated transversally to the components
 | |
|     should be kept here.
 | |
| 
 | |
|     Including:
 | |
| 
 | |
|     .. attribute:: model_name
 | |
| 
 | |
|         Name of the model we are working with. It means that any lookup for a
 | |
|         component will be done for this model. It also provides a shortcut
 | |
|         as a `model` attribute to use directly with the Odoo model from
 | |
|         the components
 | |
| 
 | |
|     .. attribute:: collection
 | |
| 
 | |
|         The collection we are working with. The collection is an Odoo
 | |
|         Model that inherit from 'collection.base'. The collection attribute
 | |
|         can be a record or an "empty" model.
 | |
| 
 | |
|     .. attribute:: model
 | |
| 
 | |
|         Odoo Model for ``model_name`` with the same Odoo
 | |
|         :class:`~odoo.api.Environment` than the ``collection`` attribute.
 | |
| 
 | |
|     This is also the entrypoint to work with the components.
 | |
| 
 | |
|     ::
 | |
| 
 | |
|         collection = self.env['my.collection'].browse(1)
 | |
|         work = WorkContext(model_name='res.partner', collection=collection)
 | |
|         component = work.component(usage='record.importer')
 | |
| 
 | |
|     Usually you will use the context manager on the ``collection.base`` Model:
 | |
| 
 | |
|     ::
 | |
| 
 | |
|         collection = self.env['my.collection'].browse(1)
 | |
|         with collection.work_on('res.partner') as work:
 | |
|             component = work.component(usage='record.importer')
 | |
| 
 | |
|     It supports any arbitrary keyword arguments that will become attributes of
 | |
|     the instance, and be propagated throughout all the components.
 | |
| 
 | |
|     ::
 | |
| 
 | |
|         collection = self.env['my.collection'].browse(1)
 | |
|         with collection.work_on('res.partner', hello='world') as work:
 | |
|             assert work.hello == 'world'
 | |
| 
 | |
|     When you need to work on a different model, a new work instance will be
 | |
|     created for you when you are using the high-level API. This is what
 | |
|     happens under the hood:
 | |
| 
 | |
|     ::
 | |
| 
 | |
|         collection = self.env['my.collection'].browse(1)
 | |
|         with collection.work_on('res.partner', hello='world') as work:
 | |
|             assert work.model_name == 'res.partner'
 | |
|             assert work.hello == 'world'
 | |
|             work2 = work.work_on('res.users')
 | |
|             # => spawn a new WorkContext with a copy of the attributes
 | |
|             assert work2.model_name == 'res.users'
 | |
|             assert work2.hello == 'world'
 | |
| 
 | |
|     """
 | |
| 
 | |
|     def __init__(self, model_name=None, collection=None,
 | |
|                  components_registry=None, **kwargs):
 | |
|         self.collection = collection
 | |
|         self.model_name = model_name
 | |
|         self.model = self.env[model_name]
 | |
|         # lookup components in an alternative registry, used by the tests
 | |
|         if components_registry is not None:
 | |
|             self.components_registry = components_registry
 | |
|         else:
 | |
|             dbname = self.env.cr.dbname
 | |
|             try:
 | |
|                 self.components_registry = _component_databases[dbname]
 | |
|             except KeyError:
 | |
|                 _logger.error(
 | |
|                     'No component registry for database %s. '
 | |
|                     'Probably because the Odoo registry has not been built '
 | |
|                     'yet.'
 | |
|                 )
 | |
|                 raise
 | |
|         self._propagate_kwargs = [
 | |
|             'collection',
 | |
|             'model_name',
 | |
|             'components_registry',
 | |
|         ]
 | |
|         for attr_name, value in kwargs.items():
 | |
|             setattr(self, attr_name, value)
 | |
|             self._propagate_kwargs.append(attr_name)
 | |
| 
 | |
|     @property
 | |
|     def env(self):
 | |
|         """ Return the current Odoo env
 | |
| 
 | |
|         This is the environment of the current collection.
 | |
|         """
 | |
|         return self.collection.env
 | |
| 
 | |
|     def work_on(self, model_name=None, collection=None):
 | |
|         """ Create a new work context for another model keeping attributes
 | |
| 
 | |
|         Used when one need to lookup components for another model.
 | |
|         """
 | |
|         kwargs = {attr_name: getattr(self, attr_name)
 | |
|                   for attr_name in self._propagate_kwargs}
 | |
|         if collection is not None:
 | |
|             kwargs['collection'] = collection
 | |
|         if model_name is not None:
 | |
|             kwargs['model_name'] = model_name
 | |
|         return self.__class__(**kwargs)
 | |
| 
 | |
|     def _component_class_by_name(self, name):
 | |
|         components_registry = self.components_registry
 | |
|         component_class = components_registry.get(name)
 | |
|         if not component_class:
 | |
|             raise NoComponentError("No component with name '%s' found." % name)
 | |
|         return component_class
 | |
| 
 | |
|     def component_by_name(self, name, model_name=None):
 | |
|         """ Return a component by its name
 | |
| 
 | |
|         If the component exists, an instance of it will be returned,
 | |
|         initialized with the current :class:`WorkContext`.
 | |
| 
 | |
|         A :exc:`odoo.addons.component.exception.NoComponentError` is raised
 | |
|         if:
 | |
| 
 | |
|         * no component with this name exists
 | |
|         * the ``_apply_on`` of the found component does not match
 | |
|           with the current working model
 | |
| 
 | |
|         In the latter case, it can be an indication that you need to switch to
 | |
|         a different model, you can do so by providing the ``model_name``
 | |
|         argument.
 | |
| 
 | |
|         """
 | |
|         if isinstance(model_name, models.BaseModel):
 | |
|             model_name = model_name._name
 | |
|         component_class = self._component_class_by_name(name)
 | |
|         work_model = model_name or self.model_name
 | |
|         if (component_class._collection and
 | |
|                 self.collection._name != component_class._collection):
 | |
|             raise NoComponentError(
 | |
|                 "Component with name '%s' can't be used for collection '%s'."
 | |
|                 (name, self.collection._name)
 | |
|             )
 | |
| 
 | |
|         if (component_class.apply_on_models and
 | |
|                 work_model not in component_class.apply_on_models):
 | |
|             if len(component_class.apply_on_models) == 1:
 | |
|                 hint_models = "'%s'" % (component_class.apply_on_models[0],)
 | |
|             else:
 | |
|                 hint_models = "<one of %r>" % (
 | |
|                     component_class.apply_on_models,
 | |
|                 )
 | |
|             raise NoComponentError(
 | |
|                 "Component with name '%s' can't be used for model '%s'.\n"
 | |
|                 "Hint: you might want to use: "
 | |
|                 "component_by_name('%s', model_name=%s)" %
 | |
|                 (name, work_model, name, hint_models)
 | |
|             )
 | |
| 
 | |
|         if work_model == self.model_name:
 | |
|             work_context = self
 | |
|         else:
 | |
|             work_context = self.work_on(model_name)
 | |
|         return component_class(work_context)
 | |
| 
 | |
|     def _lookup_components(self, usage=None, model_name=None):
 | |
|         component_classes = self.components_registry.lookup(
 | |
|             self.collection._name,
 | |
|             usage=usage,
 | |
|             model_name=model_name,
 | |
|         )
 | |
| 
 | |
|         return [cls for cls in component_classes if cls._component_match(self)]
 | |
| 
 | |
|     def component(self, usage=None, model_name=None):
 | |
|         """ Find a component by usage and model for the current collection
 | |
| 
 | |
|         It searches a component using the rules of
 | |
|         :meth:`ComponentRegistry.lookup`. When a component is found,
 | |
|         it initialize it with the current :class:`WorkContext` and returned.
 | |
| 
 | |
|         A :exc:`odoo.addons.component.exception.SeveralComponentError` is
 | |
|         raised if more than one component match for the provided
 | |
|         ``usage``/``model_name``.
 | |
| 
 | |
|         A :exc:`odoo.addons.component.exception.NoComponentError` is raised
 | |
|         if no component is found for the provided ``usage``/``model_name``.
 | |
| 
 | |
|         """
 | |
|         if isinstance(model_name, models.BaseModel):
 | |
|             model_name = model_name._name
 | |
|         model_name = model_name or self.model_name
 | |
|         component_classes = self._lookup_components(
 | |
|             usage=usage, model_name=model_name
 | |
|         )
 | |
|         if not component_classes:
 | |
|             raise NoComponentError(
 | |
|                 "No component found for collection '%s', "
 | |
|                 "usage '%s', model_name '%s'." %
 | |
|                 (self.collection._name, usage, model_name)
 | |
|             )
 | |
|         elif len(component_classes) > 1:
 | |
|             raise SeveralComponentError(
 | |
|                 "Several components found for collection '%s', "
 | |
|                 "usage '%s', model_name '%s'. Found: %r" %
 | |
|                 (self.collection._name, usage or '',
 | |
|                  model_name or '', component_classes)
 | |
|             )
 | |
|         if model_name == self.model_name:
 | |
|             work_context = self
 | |
|         else:
 | |
|             work_context = self.work_on(model_name)
 | |
|         return component_classes[0](work_context)
 | |
| 
 | |
|     def many_components(self, usage=None, model_name=None):
 | |
|         """ Find many components by usage and model for the current collection
 | |
| 
 | |
|         It searches a component using the rules of
 | |
|         :meth:`ComponentRegistry.lookup`. When components are found, they
 | |
|         initialized with the current :class:`WorkContext` and returned as a
 | |
|         list.
 | |
| 
 | |
|         If no component is found, an empty list is returned.
 | |
| 
 | |
|         """
 | |
|         if isinstance(model_name, models.BaseModel):
 | |
|             model_name = model_name._name
 | |
|         model_name = model_name or self.model_name
 | |
|         component_classes = self._lookup_components(
 | |
|             usage=usage, model_name=model_name
 | |
|         )
 | |
|         if model_name == self.model_name:
 | |
|             work_context = self
 | |
|         else:
 | |
|             work_context = self.work_on(model_name)
 | |
|         return [comp(work_context) for comp in component_classes]
 | |
| 
 | |
|     def __str__(self):
 | |
|         return "WorkContext(%s, %s)" % (self.model_name, repr(self.collection))
 | |
| 
 | |
|     __repr__ = __str__
 | |
| 
 | |
| 
 | |
| class MetaComponent(type):
 | |
|     """ Metaclass for Components
 | |
| 
 | |
|     Every new :class:`Component` will be added to ``_modules_components``,
 | |
|     that will be used by the component builder.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     _modules_components = defaultdict(list)
 | |
| 
 | |
|     def __init__(self, name, bases, attrs):
 | |
|         if not self._register:
 | |
|             self._register = True
 | |
|             super(MetaComponent, self).__init__(name, bases, attrs)
 | |
|             return
 | |
| 
 | |
|         if not hasattr(self, '_module'):
 | |
|             self._module = _get_addon_name(self.__module__)
 | |
| 
 | |
|         self._modules_components[self._module].append(self)
 | |
| 
 | |
|     @property
 | |
|     def apply_on_models(self):
 | |
|         # None means all models
 | |
|         if self._apply_on is None:
 | |
|             return None
 | |
|         # always return a list, used for the lookup
 | |
|         elif isinstance(self._apply_on, str):
 | |
|             return [self._apply_on]
 | |
|         return self._apply_on
 | |
| 
 | |
| 
 | |
| class AbstractComponent(object, metaclass=MetaComponent):
 | |
|     """ Main Component Model
 | |
| 
 | |
|     All components have a Python inheritance either on
 | |
|     :class:`AbstractComponent` or either on :class:`Component`.
 | |
| 
 | |
|     Abstract Components will not be returned by lookups on components, however
 | |
|     they can be used as a base for other Components through inheritance (using
 | |
|     ``_inherit``).
 | |
| 
 | |
|     Inheritance mechanism
 | |
|         The inheritance mechanism is like the Odoo's one for Models.  Each
 | |
|         component has a ``_name``. This is the absolute minimum in a Component
 | |
|         class.
 | |
| 
 | |
|         ::
 | |
| 
 | |
|             class MyComponent(Component):
 | |
|                 _name = 'my.component'
 | |
| 
 | |
|                 def speak(self, message):
 | |
|                     print message
 | |
| 
 | |
|         Every component implicitly inherit from the `'base'` component.
 | |
| 
 | |
|         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 component we want to extend.  With
 | |
|         the following example, ``my.component`` is now able to speak and to
 | |
|         yell.
 | |
| 
 | |
|         ::
 | |
| 
 | |
|             class MyComponent(Component):  # name of the class does not matter
 | |
|                 _inherit = 'my.component'
 | |
| 
 | |
|                 def yell(self, message):
 | |
|                     print message.upper()
 | |
| 
 | |
|         The second has a different ``_name``, it creates a new component,
 | |
|         including the behavior of the inherited component, but without
 | |
|         modifying it. In the following example, ``my.component`` is still able
 | |
|         to speak and to yell (brough by the previous inherit), but not to
 | |
|         sing.  ``another.component`` is able to speak, to yell and to sing.
 | |
| 
 | |
|         ::
 | |
| 
 | |
|             class AnotherComponent(Component):
 | |
|                 _name = 'another.component'
 | |
|                 _inherit = 'my.component'
 | |
| 
 | |
|                 def sing(self, message):
 | |
|                     print message.upper()
 | |
| 
 | |
|     Registration and lookups
 | |
|         It is handled by 3 attributes on the class:
 | |
| 
 | |
|         _collection
 | |
|             The name of the collection where we want to register the
 | |
|             component.  This is not strictly mandatory as a component can be
 | |
|             shared across several collections. But usually, you want to set a
 | |
|             collection to segregate the components for a domain.  A collection
 | |
|             can be for instance ``magento.backend``. It is also the name of a
 | |
|             model that inherits from ``collection.base``.  See also
 | |
|             :class:`~WorkContext` and
 | |
|             :class:`~odoo.addons.component.models.collection.Collection`.
 | |
| 
 | |
|         _apply_on
 | |
|             List of names or name of the Odoo model(s) for which the component
 | |
|             can be used.  When not set, the component can be used on any model.
 | |
| 
 | |
|         _usage
 | |
|            The collection and the model (``_apply_on``) will help to filter
 | |
|            the candidate components according to our working context (e.g. I'm
 | |
|            working on ``magento.backend`` with the model
 | |
|            ``magento.res.partner``).  The usage will define **what** kind of
 | |
|            task the component we are looking for serves to. For instance, it
 | |
|            might be ``record.importer``, ``export.mapper```... but you can be
 | |
|            as creative as you want.
 | |
| 
 | |
|         Now, to get a component, you'll likely use
 | |
|         :meth:`WorkContext.component` when you start to work with components
 | |
|         in your flow, but then from within your components, you are more
 | |
|         likely to use one of:
 | |
| 
 | |
|         * :meth:`component`
 | |
|         * :meth:`many_components`
 | |
|         * :meth:`component_by_name` (more rarely though)
 | |
| 
 | |
|         Declaration of some Components can look like::
 | |
| 
 | |
|             class FooBar(models.Model):
 | |
|                 _name = 'foo.bar.collection'
 | |
|                 _inherit = 'collection.base'  # this inherit is required
 | |
| 
 | |
| 
 | |
|             class FooBarBase(AbstractComponent):
 | |
|                 _name = 'foo.bar.base'
 | |
|                 _collection = 'foo.bar.collection'  # name of the model above
 | |
| 
 | |
| 
 | |
|             class Foo(Component):
 | |
|                 _name = 'foo'
 | |
|                 _inherit = 'foo.bar.base'  # we will inherit the _collection
 | |
|                 _apply_on = 'res.users'
 | |
|                 _usage = 'speak'
 | |
| 
 | |
|                 def utter(self, message):
 | |
|                     print message
 | |
| 
 | |
| 
 | |
|             class Bar(Component):
 | |
|                 _name = 'bar'
 | |
|                 _inherit = 'foo.bar.base'  # we will inherit the _collection
 | |
|                 _apply_on = 'res.users'
 | |
|                 _usage = 'yell'
 | |
| 
 | |
|                 def utter(self, message):
 | |
|                     print message.upper() + '!!!'
 | |
| 
 | |
| 
 | |
|             class Vocalizer(Component):
 | |
|                 _name = 'vocalizer'
 | |
|                 _inherit = 'foo.bar.base'
 | |
|                 _usage = 'vocalizer'
 | |
|                 # can be used for any model
 | |
| 
 | |
|                 def vocalize(action, message):
 | |
|                     self.component(usage=action).utter(message)
 | |
| 
 | |
| 
 | |
|         And their usage::
 | |
| 
 | |
|             >>> coll = self.env['foo.bar.collection'].browse(1)
 | |
|             >>> with coll.work_on('res.users') as work:
 | |
|             ...     vocalizer = work.component(usage='vocalizer')
 | |
|             ...     vocalizer.vocalize('speak', 'hello world')
 | |
|             ...
 | |
|             hello world
 | |
|             ...     vocalizer.vocalize('yell', 'hello world')
 | |
|             HELLO WORLD!!!
 | |
| 
 | |
|     Hints:
 | |
| 
 | |
|     * If you want to create components without ``_apply_on``, choose a
 | |
|       ``_usage`` that will not conflict other existing components.
 | |
|     * Unless this is what you want and in that case you use
 | |
|       :meth:`many_components` which will return all components for a usage
 | |
|       with a matching or a not set ``_apply_on``.
 | |
|     * It is advised to namespace the names of the components (e.g.
 | |
|       ``magento.xxx``) to prevent conflicts between addons.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     _register = False
 | |
|     _abstract = True
 | |
| 
 | |
|     # used for inheritance
 | |
|     _name = None
 | |
|     _inherit = None
 | |
| 
 | |
|     # name of the collection to subscribe in
 | |
|     _collection = None
 | |
| 
 | |
|     # None means any Model, can be a list ['res.users', ...]
 | |
|     _apply_on = None
 | |
|     # component purpose ('import.mapper', ...)
 | |
|     _usage = None
 | |
| 
 | |
|     def __init__(self, work_context):
 | |
|         super(AbstractComponent, self).__init__()
 | |
|         self.work = work_context
 | |
| 
 | |
|     @classmethod
 | |
|     def _component_match(cls, work):
 | |
|         """ Evaluated on candidate components
 | |
| 
 | |
|         When a component lookup is done and candidate(s) have
 | |
|         been found for a usage, a final call is done on this method.
 | |
|         If the method return False, the candidate component is ignored.
 | |
| 
 | |
|         It can be used for instance to dynamically choose a component
 | |
|         according to a value in the :class:`WorkContext`.
 | |
| 
 | |
|         Beware, if the lookups from usage, model and collection are
 | |
|         cached, the calls to :meth:`_component_match` are executed
 | |
|         each time we get components. Heavy computation should be
 | |
|         avoided.
 | |
| 
 | |
|         :param work: the :class:`WorkContext` we are working with
 | |
| 
 | |
|         """
 | |
|         return True
 | |
| 
 | |
|     @property
 | |
|     def collection(self):
 | |
|         """ Collection we are working with """
 | |
|         return self.work.collection
 | |
| 
 | |
|     @property
 | |
|     def env(self):
 | |
|         """ Current Odoo environment, the one of the collection record """
 | |
|         return self.work.env
 | |
| 
 | |
|     @property
 | |
|     def model(self):
 | |
|         """ The model instance we are working with """
 | |
|         return self.work.model
 | |
| 
 | |
|     def component_by_name(self, name, model_name=None):
 | |
|         """ Return a component by its name
 | |
| 
 | |
|         Shortcut to meth:`~WorkContext.component_by_name`
 | |
|         """
 | |
|         return self.work.component_by_name(name, model_name=model_name)
 | |
| 
 | |
|     def component(self, usage=None, model_name=None):
 | |
|         """ Return a component
 | |
| 
 | |
|         Shortcut to meth:`~WorkContext.component`
 | |
|         """
 | |
|         return self.work.component(usage=usage, model_name=model_name)
 | |
| 
 | |
|     def many_components(self, usage=None, model_name=None):
 | |
|         """ Return several components
 | |
| 
 | |
|         Shortcut to meth:`~WorkContext.many_components`
 | |
|         """
 | |
|         return self.work.many_components(usage=usage, model_name=model_name)
 | |
| 
 | |
|     def __str__(self):
 | |
|         return "Component(%s)" % self._name
 | |
| 
 | |
|     __repr__ = __str__
 | |
| 
 | |
|     @classmethod
 | |
|     def _build_component(cls, registry):
 | |
|         """ Instantiate a given Component in the components registry.
 | |
| 
 | |
|         This method is called at the end of the Odoo's registry build.  The
 | |
|         caller is :meth:`component.builder.ComponentBuilder.load_components`.
 | |
| 
 | |
|         It generates new classes, which will be the Component classes we will
 | |
|         be using.  The new classes are generated following the inheritance
 | |
|         of ``_inherit``. It ensures that the ``__bases__`` of the generated
 | |
|         Component classes follow the ``_inherit`` chain.
 | |
| 
 | |
|         Once a Component class is created, it adds it in the Component Registry
 | |
|         (:class:`ComponentRegistry`), so it will be available for
 | |
|         lookups.
 | |
| 
 | |
|         At the end of new class creation, a hook method
 | |
|         :meth:`_complete_component_build` is called, so you can customize
 | |
|         further the created components. An example can be found in
 | |
|         :meth:`odoo.addons.connector.components.mapper.Mapper._complete_component_build`
 | |
| 
 | |
|         The following code is roughly the same than the Odoo's one for
 | |
|         building Models.
 | |
| 
 | |
|         """
 | |
| 
 | |
|         # In the simplest case, the component's registry class inherits from
 | |
|         # cls and the other classes that define the component in a flat
 | |
|         # hierarchy.  The registry contains the instance ``component`` (on the
 | |
|         # left). Its class, ``ComponentClass``, carries inferred metadata that
 | |
|         # is shared between all the component's instances for this registry
 | |
|         # only.
 | |
|         #
 | |
|         #   class A1(Component):                    Component
 | |
|         #       _name = 'a'                           / | \
 | |
|         #                                            A3 A2 A1
 | |
|         #   class A2(Component):                      \ | /
 | |
|         #       _inherit = 'a'                    ComponentClass
 | |
|         #
 | |
|         #   class A3(Component):
 | |
|         #       _inherit = 'a'
 | |
|         #
 | |
|         # When a component is extended by '_inherit', its base classes are
 | |
|         # modified to include the current class and the other inherited
 | |
|         # component classes.
 | |
|         # Note that we actually inherit from other ``ComponentClass``, so that
 | |
|         # extensions to an inherited component are immediately visible in the
 | |
|         # current component class, like in the following example:
 | |
|         #
 | |
|         #   class A1(Component):
 | |
|         #       _name = 'a'                          Component
 | |
|         #                                            /  / \  \
 | |
|         #   class B1(Component):                    /  A2 A1  \
 | |
|         #       _name = 'b'                        /   \  /    \
 | |
|         #                                         B2 ComponentA B1
 | |
|         #   class B2(Component):                   \     |     /
 | |
|         #       _name = 'b'                         \    |    /
 | |
|         #       _inherit = ['b', 'a']                \   |   /
 | |
|         #                                            ComponentB
 | |
|         #   class A2(Component):
 | |
|         #       _inherit = 'a'
 | |
| 
 | |
|         # determine inherited components
 | |
|         parents = cls._inherit
 | |
|         if isinstance(parents, str):
 | |
|             parents = [parents]
 | |
|         elif parents is None:
 | |
|             parents = []
 | |
| 
 | |
|         if cls._name in registry and not parents:
 | |
|             raise TypeError('Component %r (in class %r) already exists. '
 | |
|                             'Consider using _inherit instead of _name '
 | |
|                             'or using a different _name.' % (cls._name, cls))
 | |
| 
 | |
|         # determine the component's name
 | |
|         name = cls._name or (len(parents) == 1 and parents[0])
 | |
| 
 | |
|         if not name:
 | |
|             raise TypeError('Component %r must have a _name' % cls)
 | |
| 
 | |
|         # all components except 'base' implicitly inherit from 'base'
 | |
|         if name != 'base':
 | |
|             parents = list(parents) + ['base']
 | |
| 
 | |
|         # create or retrieve the component's class
 | |
|         if name in parents:
 | |
|             if name not in registry:
 | |
|                 raise TypeError("Component %r does not exist in registry." %
 | |
|                                 name)
 | |
|             ComponentClass = registry[name]
 | |
|             ComponentClass._build_component_check_base(cls)
 | |
|             check_parent = ComponentClass._build_component_check_parent
 | |
|         else:
 | |
|             ComponentClass = type(
 | |
|                 name, (AbstractComponent,),
 | |
|                 {'_name': name,
 | |
|                  '_register': False,
 | |
|                  # names of children component
 | |
|                  '_inherit_children': OrderedSet()},
 | |
|             )
 | |
|             check_parent = cls._build_component_check_parent
 | |
| 
 | |
|         # determine all the classes the component should inherit from
 | |
|         bases = LastOrderedSet([cls])
 | |
|         for parent in parents:
 | |
|             if parent not in registry:
 | |
|                 raise TypeError(
 | |
|                     "Component %r inherits from non-existing component %r." %
 | |
|                     (name, parent)
 | |
|                 )
 | |
|             parent_class = registry[parent]
 | |
|             if parent == name:
 | |
|                 for base in parent_class.__bases__:
 | |
|                     bases.add(base)
 | |
|             else:
 | |
|                 check_parent(cls, parent_class)
 | |
|                 bases.add(parent_class)
 | |
|                 parent_class._inherit_children.add(name)
 | |
|         ComponentClass.__bases__ = tuple(bases)
 | |
| 
 | |
|         ComponentClass._complete_component_build()
 | |
| 
 | |
|         registry[name] = ComponentClass
 | |
| 
 | |
|         return ComponentClass
 | |
| 
 | |
|     @classmethod
 | |
|     def _build_component_check_base(cls, extend_cls):
 | |
|         """ Check whether ``cls`` can be extended with ``extend_cls``. """
 | |
|         if cls._abstract and not extend_cls._abstract:
 | |
|             msg = ("%s transforms the abstract component %r into a "
 | |
|                    "non-abstract component. "
 | |
|                    "That class should either inherit from AbstractComponent, "
 | |
|                    "or set a different '_name'.")
 | |
|             raise TypeError(msg % (extend_cls, cls._name))
 | |
| 
 | |
|     @classmethod
 | |
|     def _build_component_check_parent(component_class, cls, parent_class):
 | |
|         """ Check whether ``model_class`` can inherit from ``parent_class``.
 | |
|         """
 | |
|         if component_class._abstract and not parent_class._abstract:
 | |
|             msg = ("In %s, the abstract Component %r cannot inherit "
 | |
|                    "from the non-abstract Component %r.")
 | |
|             raise TypeError(
 | |
|                 msg % (cls, component_class._name, parent_class._name)
 | |
|             )
 | |
| 
 | |
|     @classmethod
 | |
|     def _complete_component_build(cls):
 | |
|         """ Complete build of the new component class
 | |
| 
 | |
|         After the component 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 Component, but a Component can inherit
 | |
|         the method to add its own behavior.
 | |
|         """
 | |
| 
 | |
| 
 | |
| class Component(AbstractComponent):
 | |
|     """ Concrete Component class
 | |
| 
 | |
|     This is the class you inherit from when you want your component to
 | |
|     be registered in the component collections.
 | |
| 
 | |
|     Look in :class:`AbstractComponent` for more details.
 | |
| 
 | |
|     """
 | |
|     _register = False
 | |
|     _abstract = False
 |