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()
|