295 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			295 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Python
		
	
	
# -*- coding: utf-8 -*-
 | 
						|
# Copyright 2017 Camptocamp SA
 | 
						|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
 | 
						|
 | 
						|
"""
 | 
						|
Events
 | 
						|
======
 | 
						|
 | 
						|
Events are a notification system.
 | 
						|
 | 
						|
On one side, one or many listeners await for an event to happen. On
 | 
						|
the other side, when such event happen, a notification is sent to
 | 
						|
the listeners.
 | 
						|
 | 
						|
An example of event is: 'when a record has been created'.
 | 
						|
 | 
						|
The event system allows to write the notification code in only one place, in
 | 
						|
one Odoo addon, and to write as many listeners as we want, in different places,
 | 
						|
different addons.
 | 
						|
 | 
						|
We'll see below how the ``on_record_create`` is implemented.
 | 
						|
 | 
						|
Notifier
 | 
						|
--------
 | 
						|
 | 
						|
The first thing is to find where/when the notification should be sent.
 | 
						|
For the creation of a record, it is in :meth:`odoo.models.BaseModel.create`.
 | 
						|
We can inherit from the `'base'` model to add this line:
 | 
						|
 | 
						|
::
 | 
						|
 | 
						|
    class Base(models.AbstractModel):
 | 
						|
        _inherit = 'base'
 | 
						|
 | 
						|
        @api.model
 | 
						|
        def create(self, vals):
 | 
						|
            record = super(Base, self).create(vals)
 | 
						|
            self._event('on_record_create').notify(record, fields=vals.keys())
 | 
						|
            return record
 | 
						|
 | 
						|
The :meth:`..models.base.Base._event` method has been added to the `'base'`
 | 
						|
model, so an event can be notified from any model. The
 | 
						|
:meth:`CollectedEvents.notify` method triggers the event and forward the
 | 
						|
arguments to the listeners.
 | 
						|
 | 
						|
This should be done only once. See :class:`..models.base.Base` for a list of
 | 
						|
events that are implemented in the `'base'` model.
 | 
						|
 | 
						|
Listeners
 | 
						|
---------
 | 
						|
 | 
						|
Listeners are Components that respond to the event names.
 | 
						|
The components must have a ``_usage`` equals to ``'event.listener'``, but it
 | 
						|
doesn't to be set manually if the component inherits from
 | 
						|
``'base.event.listener'``
 | 
						|
 | 
						|
Here is how we would log something each time a record is created::
 | 
						|
 | 
						|
    class MyEventListener(Component):
 | 
						|
        _name = 'my.event.listener'
 | 
						|
        _inherit = 'base.event.listener'
 | 
						|
 | 
						|
        def on_record_create(self, record, fields=None):
 | 
						|
            _logger.info("%r has been created", record)
 | 
						|
 | 
						|
Many listeners such as this one could be added for the same event.
 | 
						|
 | 
						|
 | 
						|
Collection and models
 | 
						|
---------------------
 | 
						|
 | 
						|
In the example above, the listeners is global. It will be executed for any
 | 
						|
model and collection. You can also restrict a listener to only a collection or
 | 
						|
model, using the ``_collection`` or ``_apply_on`` attributes.
 | 
						|
 | 
						|
::
 | 
						|
 | 
						|
    class MyEventListener(Component):
 | 
						|
        _name = 'my.event.listener'
 | 
						|
        _inherit = 'base.event.listener'
 | 
						|
        _collection = 'magento.backend'
 | 
						|
 | 
						|
        def on_record_create(self, record, fields=None):
 | 
						|
            _logger.info("%r has been created", record)
 | 
						|
 | 
						|
 | 
						|
    class MyModelEventListener(Component):
 | 
						|
        _name = 'my.event.listener'
 | 
						|
        _inherit = 'base.event.listener'
 | 
						|
        _apply_on = ['res.users']
 | 
						|
 | 
						|
        def on_record_create(self, record, fields=None):
 | 
						|
            _logger.info("%r has been created", record)
 | 
						|
 | 
						|
 | 
						|
If you want an event to be restricted to a collection, the
 | 
						|
notification must also precise the collection, otherwise all listeners
 | 
						|
will be executed::
 | 
						|
 | 
						|
 | 
						|
    collection = self.env['magento.backend']
 | 
						|
    self._event('on_foo_created', collection=collection).notify(record, vals)
 | 
						|
 | 
						|
An event can be skipped based on a condition evaluated from the notified
 | 
						|
arguments. See :func:`skip_if`
 | 
						|
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
import logging
 | 
						|
import operator
 | 
						|
 | 
						|
from collections import defaultdict
 | 
						|
from functools import wraps
 | 
						|
 | 
						|
from odoo.addons.component.core import AbstractComponent, Component
 | 
						|
 | 
						|
_logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
try:
 | 
						|
    from cachetools import LRUCache, cachedmethod, keys
 | 
						|
except ImportError:
 | 
						|
    _logger.debug("Cannot import 'cachetools'.")
 | 
						|
 | 
						|
# Number of items we keep in LRU cache when we collect the events.
 | 
						|
# 1 item means: for an event name, model_name, collection, return
 | 
						|
# the event methods
 | 
						|
DEFAULT_EVENT_CACHE_SIZE = 512
 | 
						|
 | 
						|
 | 
						|
def skip_if(cond):
 | 
						|
    """ Decorator allowing to skip an event based on a condition
 | 
						|
 | 
						|
    The condition is a python lambda expression, which takes the
 | 
						|
    same arguments than the event.
 | 
						|
 | 
						|
    Example::
 | 
						|
 | 
						|
        @skip_if(lambda self, *args, **kwargs:
 | 
						|
                 self.env.context.get('connector_no_export'))
 | 
						|
        def on_record_write(self, record, fields=None):
 | 
						|
            _logger('I'll delay a job, but only if we didn't disabled '
 | 
						|
                    ' the export with a context key')
 | 
						|
            record.with_delay().export_record()
 | 
						|
 | 
						|
        @skip_if(lambda self, record, kind: kind == 'complete')
 | 
						|
        def on_record_write(self, record, kind):
 | 
						|
            _logger("I'll delay a job, but only if the kind is 'complete'")
 | 
						|
            record.with_delay().export_record()
 | 
						|
 | 
						|
    """
 | 
						|
    def skip_if_decorator(func):
 | 
						|
        @wraps(func)
 | 
						|
        def func_wrapper(*args, **kwargs):
 | 
						|
            if cond(*args, **kwargs):
 | 
						|
                return
 | 
						|
            else:
 | 
						|
                return func(*args, **kwargs)
 | 
						|
 | 
						|
        return func_wrapper
 | 
						|
    return skip_if_decorator
 | 
						|
 | 
						|
 | 
						|
class CollectedEvents(object):
 | 
						|
    """ Event methods ready to be notified
 | 
						|
 | 
						|
    This is a rather internal class. An instance of this class
 | 
						|
    is prepared by the :class:`EventCollecter` when we need to notify
 | 
						|
    the listener that the event has been triggered.
 | 
						|
 | 
						|
    :meth:`EventCollecter.collect_events` collects the events,
 | 
						|
    feed them to the instance, so we can use the :meth:`notify` method
 | 
						|
    that will forward the arguments and keyword arguments to the
 | 
						|
    listeners of the event.
 | 
						|
    ::
 | 
						|
 | 
						|
        >>> # collecter is an instance of CollectedEvents
 | 
						|
        >>> collecter.collect_events('on_record_create').notify(something)
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, events):
 | 
						|
        self.events = events
 | 
						|
 | 
						|
    def notify(self, *args, **kwargs):
 | 
						|
        """ Forward the arguments to every listeners of an event """
 | 
						|
        for event in self.events:
 | 
						|
            event(*args, **kwargs)
 | 
						|
 | 
						|
 | 
						|
class EventCollecter(Component):
 | 
						|
    """ Component that collects the event from an event name
 | 
						|
 | 
						|
    For doing so, it searches all the components that respond to the
 | 
						|
    ``event.listener`` ``_usage`` and having an event of the same
 | 
						|
    name.
 | 
						|
 | 
						|
    Then it feeds the events to an instance of :class:`EventCollecter`
 | 
						|
    and return it to the caller.
 | 
						|
 | 
						|
    It keeps the results in a cache, the Component is rebuilt when
 | 
						|
    the Odoo's registry is rebuilt, hence the cache is cleared as well.
 | 
						|
 | 
						|
    An event always starts with ``on_``.
 | 
						|
 | 
						|
    Note that the special
 | 
						|
    :class:`odoo.addons.component_event.core.EventWorkContext` class should be
 | 
						|
    used for this Component, because it can work
 | 
						|
    without a collection.
 | 
						|
 | 
						|
    It is used by :meth:`odoo.addons.component_event.models.base.Base._event`.
 | 
						|
 | 
						|
    """
 | 
						|
    _name = 'base.event.collecter'
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _complete_component_build(cls):
 | 
						|
        """ Create a cache on the class when the component is built """
 | 
						|
        super(EventCollecter, cls)._complete_component_build()
 | 
						|
        # the _cache being on the component class, which is
 | 
						|
        # dynamically rebuild when odoo registry is rebuild, we
 | 
						|
        # are sure that the result is always the same for a lookup
 | 
						|
        # until the next rebuild of odoo's registry
 | 
						|
        cls._cache = LRUCache(maxsize=DEFAULT_EVENT_CACHE_SIZE)
 | 
						|
 | 
						|
    @cachedmethod(operator.attrgetter('_cache'),
 | 
						|
                  key=lambda self, name: keys.hashkey(
 | 
						|
                      self.work.collection._name
 | 
						|
                      if self.work._collection is not None else None,
 | 
						|
                      self.work.model_name,
 | 
						|
                      name)
 | 
						|
                  )
 | 
						|
    def _collect_events(self, name):
 | 
						|
        events = defaultdict(set)
 | 
						|
        collection_name = (self.work.collection._name
 | 
						|
                           if self.work._collection is not None
 | 
						|
                           else None)
 | 
						|
        component_classes = self.work.components_registry.lookup(
 | 
						|
            collection_name=collection_name,
 | 
						|
            usage='event.listener',
 | 
						|
            model_name=self.work.model_name,
 | 
						|
        )
 | 
						|
        for cls in component_classes:
 | 
						|
            if cls.has_event(name):
 | 
						|
                events[cls].add(name)
 | 
						|
        return events
 | 
						|
 | 
						|
    def _init_collected_events(self, class_events):
 | 
						|
        events = set()
 | 
						|
        for cls, names in class_events.items():
 | 
						|
            for name in names:
 | 
						|
                component = cls(self.work)
 | 
						|
                events.add(getattr(component, name))
 | 
						|
        return events
 | 
						|
 | 
						|
    def collect_events(self, name):
 | 
						|
        """ Collect the events of a given name """
 | 
						|
        if not name.startswith('on_'):
 | 
						|
            raise ValueError("an event name always starts with 'on_'")
 | 
						|
 | 
						|
        events = self._init_collected_events(self._collect_events(name))
 | 
						|
        return CollectedEvents(events)
 | 
						|
 | 
						|
 | 
						|
class EventListener(AbstractComponent):
 | 
						|
    """ Base Component for the Event listeners
 | 
						|
 | 
						|
    Events must be methods starting with ``on_``.
 | 
						|
 | 
						|
    Example: :class:`RecordsEventListener`
 | 
						|
 | 
						|
    """
 | 
						|
    _name = 'base.event.listener'
 | 
						|
    _usage = 'event.listener'
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def has_event(cls, name):
 | 
						|
        """ Indicate if the class has an event of this name """
 | 
						|
        return name in cls._events
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _build_event_listener_component(cls):
 | 
						|
        """ Make a list of events listeners for this class """
 | 
						|
        events = set([])
 | 
						|
        if not cls._abstract:
 | 
						|
            for attr_name in dir(cls):
 | 
						|
                if attr_name.startswith('on_'):
 | 
						|
                    events.add(attr_name)
 | 
						|
        cls._events = events
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _complete_component_build(cls):
 | 
						|
        super(EventListener, cls)._complete_component_build()
 | 
						|
        cls._build_event_listener_component()
 |