From f6cb67df50193c359a994113f9c51eb837fd9fbf Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sat, 17 Jun 2017 22:20:49 +0200 Subject: [PATCH 01/80] Draft new component_event addon Proposing a new API based on components for the events --- component_event/__init__.py | 4 + component_event/__manifest__.py | 17 ++ component_event/components/__init__.py | 2 + component_event/components/event.py | 103 ++++++++++ component_event/core.py | 59 ++++++ component_event/models/__init__.py | 2 + component_event/models/base.py | 27 +++ component_event/tests/__init__.py | 2 + component_event/tests/test_event.py | 274 +++++++++++++++++++++++++ 9 files changed, 490 insertions(+) create mode 100644 component_event/__init__.py create mode 100644 component_event/__manifest__.py create mode 100644 component_event/components/__init__.py create mode 100644 component_event/components/event.py create mode 100644 component_event/core.py create mode 100644 component_event/models/__init__.py create mode 100644 component_event/models/base.py create mode 100644 component_event/tests/__init__.py create mode 100644 component_event/tests/test_event.py diff --git a/component_event/__init__.py b/component_event/__init__.py new file mode 100644 index 000000000..6a0f974d1 --- /dev/null +++ b/component_event/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from . import core +from . import components +from . import models diff --git a/component_event/__manifest__.py b/component_event/__manifest__.py new file mode 100644 index 000000000..169dbd11c --- /dev/null +++ b/component_event/__manifest__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +{'name': 'Components Events', + 'version': '10.0.1.0.0', + 'author': 'Camptocamp,' + 'Odoo Community Association (OCA)', + 'website': 'https://www.camptocamp.com', + 'license': 'AGPL-3', + 'category': 'Generic Modules', + 'depends': ['component', + ], + 'data': [], + 'installable': True, + } + diff --git a/component_event/components/__init__.py b/component_event/components/__init__.py new file mode 100644 index 000000000..7351619d9 --- /dev/null +++ b/component_event/components/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import event diff --git a/component_event/components/event.py b/component_event/components/event.py new file mode 100644 index 000000000..46a346422 --- /dev/null +++ b/component_event/components/event.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +""" +Events +====== + +TODO + +""" + +from odoo.addons.component.core import AbstractComponent, Component + + +class EventProducer(AbstractComponent): + _name = 'base.event.producer' + + def __init__(self, work): + super(EventProducer, self).__init__(work) + self._events = set() + + def collect_events(self, name): + component_classes = self.work._components_registry.lookup( + usage='event.listener', + model_name=self.model._name, + ) + for cls in component_classes: + if cls.has_event(name): + component = cls(self.work) + self._events.add(getattr(component, name)) + return self + + def fire(self, *args, **kwargs): + for event in self._events: + event(*args, **kwargs) + + +class EventListener(AbstractComponent): + """ Base Component for the Event listeners + + Events must be methods starting with ``on_``. + + Example: :class:`RecordsEventListener` + + Inside an event method, you can access to the record or records that + triggered the event using ``self.recordset``. + + """ + _name = 'base.event.listener' + _usage = 'event.listener' + + @classmethod + def has_event(cls, name): + return name in cls._events + + @classmethod + def _build_event_listener_component(cls): + events = set([]) + if not cls._abstract: + for attr_name in dir(cls): + if attr_name.startswith('on_'): + # possible future optimization: store all events in a + # registry so we don't need to loop on event.listener + # components + events.add(attr_name) + cls._events = events + + @classmethod + def _complete_component_build(cls): + super(EventListener, cls)._complete_component_build() + cls._build_event_listener_component() + + @property + def recordset(self): + """ Recordset that triggered the event """ + return getattr(self.work, 'from_recordset', None) + + +class RecordsEventListener(Component): + _name = 'records.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self, fields=None): + """ Called when a record is created + + The record that triggered the event is in ``self.record``. + + """ + + def on_record_write(self, fields=None): + """ Called when a record is modified + + The record that triggered the event is in ``self.record``. + + """ + + def on_record_unlink(self, fields=None): + """ Called when a record is deleted + + The record that triggered the event is in ``self.record``. + + """ diff --git a/component_event/core.py b/component_event/core.py new file mode 100644 index 000000000..9b5e32b9b --- /dev/null +++ b/component_event/core.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +from odoo.addons.component.core import WorkContext + + +class EventWorkContext(WorkContext): + """ TODO + + """ + + def __init__(self, model_name=None, collection=None, env=None, + from_recordset=None, + components_registry=None, **kwargs): + if not (collection or env): + raise ValueError('collection or env is required') + if collection and env: + # when a collection is used, the env will be the one of + # the collection + raise ValueError('collection and env cannot both be provided') + + self.env = env + super(EventWorkContext, self).__init__( + model_name=model_name, collection=collection, + components_registry=components_registry, + from_recordset=from_recordset, + **kwargs + ) + if self._env: + self._propagate_kwargs.remove('collection') + self._propagate_kwargs.append('env') + + @property + def env(self): + """ Return the current Odoo env """ + if self._env: + return self._env + return super(EventWorkContext, self).env + + @env.setter + def env(self, value): + self._env = value + + @property + def collection(self): + """ Return the current Odoo env """ + if self._collection: + return self._collection + raise ValueError('No collection, it is optional for EventWorkContext') + + @collection.setter + def collection(self, value): + self._collection = value + + def __str__(self): + return ("EventWorkContext(%s,%s)" % + (repr(self._env or self._collection), self.model_name)) diff --git a/component_event/models/__init__.py b/component_event/models/__init__.py new file mode 100644 index 000000000..a61d43eb9 --- /dev/null +++ b/component_event/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import base diff --git a/component_event/models/base.py b/component_event/models/base.py new file mode 100644 index 000000000..7f277575d --- /dev/null +++ b/component_event/models/base.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo import models +from ..core import EventWorkContext + + +class Base(models.AbstractModel): + """ The base model, which is implicitly inherited by all models. """ + _inherit = 'base' + + def _event(self, name, model_name=None, collection=None, + components_registry=None): + model_name = model_name or self._name + if collection: + work = EventWorkContext(collection=collection, + model_name=model_name, + from_recordset=self, + components_registry=components_registry) + else: + work = EventWorkContext(env=self.env, model_name=model_name, + from_recordset=self, + components_registry=components_registry) + + producer = work.component_by_name('base.event.producer') + return producer.collect_events(name) diff --git a/component_event/tests/__init__.py b/component_event/tests/__init__.py new file mode 100644 index 000000000..3fa87e4de --- /dev/null +++ b/component_event/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_event diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py new file mode 100644 index 000000000..50156c90b --- /dev/null +++ b/component_event/tests/test_event.py @@ -0,0 +1,274 @@ +# -*- coding: utf-8 -*- +# Copyright 2013-2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import mock +import unittest2 + +from odoo.addons.component.tests.common import ( + ComponentRegistryCase, + TransactionComponentRegistryCase, +) +from odoo.addons.component.core import Component +from odoo.addons.component_event.core import EventWorkContext +from odoo.addons.component_event.components.event import ( + EventProducer, + EventListener, +) + + +class TestEventWorkContext(unittest2.TestCase): + """ Test Events Components """ + + def test_env(self): + """ WorkContext with env """ + env = mock.MagicMock(name='env') + record = mock.MagicMock(name='record') + work = EventWorkContext(model_name='res.users', env=env, + from_recordset=record) + self.assertEquals(env, work.env) + self.assertEquals('res.users', work.model_name) + self.assertEquals(record, work.from_recordset) + with self.assertRaises(ValueError): + work.collection + + def test_collection(self): + """ WorkContext with collection """ + env = mock.MagicMock(name='env') + collection = mock.MagicMock(name='collection') + collection.env = env + record = mock.MagicMock(name='record') + work = EventWorkContext(model_name='res.users', collection=collection, + from_recordset=record) + self.assertEquals(collection, work.collection) + self.assertEquals(record, work.from_recordset) + self.assertEquals(env, work.env) + self.assertEquals('res.users', work.model_name) + + def test_env_and_collection(self): + """ WorkContext with collection and env is forbidden """ + env = mock.MagicMock(name='env') + collection = mock.MagicMock(name='collection') + collection.env = env + with self.assertRaises(ValueError): + EventWorkContext(model_name='res.users', collection=collection, + env=env) + + def test_missing(self): + """ WorkContext with collection and env is forbidden """ + with self.assertRaises(ValueError): + EventWorkContext(model_name='res.users') + + def test_env_work_on(self): + """ WorkContext propagated through work_on """ + env = mock.MagicMock(name='env') + record = mock.MagicMock(name='record') + work = EventWorkContext(env=env, model_name='res.users', + from_recordset=record) + work2 = work.work_on('res.partner') + self.assertEquals('EventWorkContext', work2.__class__.__name__) + self.assertEquals(env, work2.env) + self.assertEquals(record, work2.from_recordset) + self.assertEquals('res.partner', work2.model_name) + with self.assertRaises(ValueError): + work.collection + + def test_collection_work_on(self): + """ WorkContext propagated through work_on """ + env = mock.MagicMock(name='env') + collection = mock.MagicMock(name='collection') + collection.env = env + record = mock.MagicMock(name='record') + work = EventWorkContext(collection=collection, model_name='res.users', + from_recordset=record) + work2 = work.work_on('res.partner') + self.assertEquals('EventWorkContext', work2.__class__.__name__) + self.assertEquals(collection, work2.collection) + self.assertEquals(record, work2.from_recordset) + self.assertEquals(env, work2.env) + self.assertEquals('res.partner', work2.model_name) + + +class TestEvent(ComponentRegistryCase): + """ Test Events Components """ + + def setUp(self): + super(TestEvent, self).setUp() + # build and push in the component registry the base components we + # inherit from in the tests + # 'base.event.producer' + EventProducer._build_component(self.comp_registry) + # 'base.event.listener' + EventListener._build_component(self.comp_registry) + + # get the producer to fire the event + # we don't mind about the collection and the model here, + # the events we test are global + env = mock.MagicMock() + work = EventWorkContext(model_name='res.users', env=env, + components_registry=self.comp_registry) + self.producer = self.comp_registry['base.event.producer'](work) + + def test_event(self): + class MyEventListener(Component): + _name = 'my.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self, recipient, something, fields=None): + recipient.append(('OK', something, fields)) + + MyEventListener._build_component(self.comp_registry) + + something = object() + fields = ['name', 'code'] + + # as there is no return value by the event, we + # modify this recipient to check it has been called + recipient = [] + + # collect the event and fire it + self.producer.collect_events('on_record_create').fire( + recipient, something, fields=fields + ) + self.assertEquals([('OK', something, fields)], recipient) + + def test_collect_several(self): + class MyEventListener(Component): + _name = 'my.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self, recipient, something, fields=None): + recipient.append(('OK', something, fields)) + + class MyOtherEventListener(Component): + _name = 'my.other.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self, recipient, something, fields=None): + recipient.append(('OK', something, fields)) + + MyEventListener._build_component(self.comp_registry) + MyOtherEventListener._build_component(self.comp_registry) + + something = object() + fields = ['name', 'code'] + + # as there is no return value by the event, we + # modify this recipient to check it has been called + recipient = [] + + # collect the event and fire them + events = self.producer.collect_events('on_record_create') + self.assertEquals(2, len(self.producer._events)) + + events.fire(recipient, something, fields=fields) + self.assertEquals([('OK', something, fields), + ('OK', something, fields)], recipient) + + +class TestEventRecordset(ComponentRegistryCase): + """ Test Events with Recordset """ + + def setUp(self): + super(TestEventRecordset, self).setUp() + # build and push in the component registry the base components we + # inherit from in the tests + # 'base.event.producer' + EventProducer._build_component(self.comp_registry) + # 'base.event.listener' + EventListener._build_component(self.comp_registry) + + # get the producer to fire the event + # we don't mind about the collection and the model here, + # the events we test are global + env = mock.MagicMock() + self.recordset = mock.MagicMock(name='recordset') + # when there is a 'from_recordset' in the WorkContext, + # the listener methods will be able to access to it from + # self.recordset. It is used when events are triggered from + # records using BaseModel._event + work = EventWorkContext(model_name='res.users', env=env, + from_recordset=self.recordset, + components_registry=self.comp_registry) + self.producer = self.comp_registry['base.event.producer'](work) + + def test_event(self): + class MyEventListener(Component): + _name = 'my.event.listener' + _inherit = 'base.event.listener' + + def on_foo(self, msg): + self.recordset.msg = msg + + MyEventListener._build_component(self.comp_registry) + + # collect the event and fire it + self.producer.collect_events('on_foo').fire('OK') + self.assertEquals('OK', self.recordset.msg) + + +class TestEventFromModel(TransactionComponentRegistryCase): + """ Test Events Components from Models """ + + def setUp(self): + super(TestEventFromModel, self).setUp() + # build and push in the component registry the base components we + # inherit from in the tests + # 'base.event.producer' + EventProducer._build_component(self.comp_registry) + # 'base.event.listener' + EventListener._build_component(self.comp_registry) + + def test_event_from_model(self): + class MyEventListener(Component): + _name = 'my.event.listener' + _inherit = 'base.event.listener' + + def on_foo(self, name): + self.recordset.name = name + + MyEventListener._build_component(self.comp_registry) + + partner = self.env['res.partner'].create({'name': 'test'}) + # Normally you would not pass a components_registry, + # this is for the sake of the test, letting it empty + # will use the global registry. + # In a real code it would look like: + # partner._event('on_foo').fire('bar') + events = partner._event('on_foo', + components_registry=self.comp_registry) + events.fire('bar') + self.assertEquals('bar', partner.name) + + def test_event_filter_on_model(self): + class GlobalListener(Component): + _name = 'global.event.listener' + _inherit = 'base.event.listener' + + def on_foo(self, name): + self.recordset.name = name + + class PartnerListener(Component): + _name = 'partner.event.listener' + _inherit = 'base.event.listener' + _apply_on = ['res.partner'] + + def on_foo(self, name): + self.recordset.ref = name + + class UserListener(Component): + _name = 'user.event.listener' + _inherit = 'base.event.listener' + _apply_on = ['res.users'] + + def on_foo(self, name): + assert False + + self._build_components(GlobalListener, PartnerListener, UserListener) + + partner = self.env['res.partner'].create({'name': 'test'}) + partner._event('on_foo', + components_registry=self.comp_registry).fire('bar') + self.assertEquals('bar', partner.name) + self.assertEquals('bar', partner.ref) + From f98601b849081c55d38e3dfd44b8557082951570 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sat, 17 Jun 2017 23:10:13 +0200 Subject: [PATCH 02/80] Hold a component registry per database Because they can have different addons --- component_event/components/event.py | 12 +++++++++- component_event/tests/test_event.py | 34 +++++++++++++++++++---------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index 46a346422..9496a6a7f 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -21,7 +21,7 @@ def __init__(self, work): self._events = set() def collect_events(self, name): - component_classes = self.work._components_registry.lookup( + component_classes = self.work.components_registry.lookup( usage='event.listener', model_name=self.model._name, ) @@ -76,6 +76,16 @@ def recordset(self): """ Recordset that triggered the event """ return getattr(self.work, 'from_recordset', None) + # TODO: error if we don't have the collection + def component_by_name(self, name, model_name=None): + raise NotImplementedError + + def component(self, usage=None, model_name=None): + raise NotImplementedError + + def many_components(self, usage=None, model_name=None): + raise NotImplementedError + class RecordsEventListener(Component): _name = 'records.event.listener' diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index 50156c90b..a7d914ae5 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -20,15 +20,20 @@ class TestEventWorkContext(unittest2.TestCase): """ Test Events Components """ + def setUp(self): + super(TestEventWorkContext, self).setUp() + self.env = mock.MagicMock(name='env') + self.record = mock.MagicMock(name='record') + self.components_registry = mock.MagicMock(name='ComponentRegistry') + def test_env(self): """ WorkContext with env """ - env = mock.MagicMock(name='env') - record = mock.MagicMock(name='record') - work = EventWorkContext(model_name='res.users', env=env, - from_recordset=record) - self.assertEquals(env, work.env) + work = EventWorkContext(model_name='res.users', env=self.env, + from_recordset=self.record, + components_registry=self.components_registry) + self.assertEquals(self.env, work.env) self.assertEquals('res.users', work.model_name) - self.assertEquals(record, work.from_recordset) + self.assertEquals(self.record, work.from_recordset) with self.assertRaises(ValueError): work.collection @@ -39,7 +44,8 @@ def test_collection(self): collection.env = env record = mock.MagicMock(name='record') work = EventWorkContext(model_name='res.users', collection=collection, - from_recordset=record) + from_recordset=record, + components_registry=self.components_registry) self.assertEquals(collection, work.collection) self.assertEquals(record, work.from_recordset) self.assertEquals(env, work.env) @@ -52,24 +58,28 @@ def test_env_and_collection(self): collection.env = env with self.assertRaises(ValueError): EventWorkContext(model_name='res.users', collection=collection, - env=env) + env=env, + components_registry=self.components_registry) def test_missing(self): """ WorkContext with collection and env is forbidden """ with self.assertRaises(ValueError): - EventWorkContext(model_name='res.users') + EventWorkContext(model_name='res.users', + components_registry=self.components_registry) def test_env_work_on(self): """ WorkContext propagated through work_on """ env = mock.MagicMock(name='env') record = mock.MagicMock(name='record') work = EventWorkContext(env=env, model_name='res.users', - from_recordset=record) + from_recordset=record, + components_registry=self.components_registry) work2 = work.work_on('res.partner') self.assertEquals('EventWorkContext', work2.__class__.__name__) self.assertEquals(env, work2.env) self.assertEquals(record, work2.from_recordset) self.assertEquals('res.partner', work2.model_name) + self.assertEquals(self.components_registry, work2.components_registry) with self.assertRaises(ValueError): work.collection @@ -80,13 +90,15 @@ def test_collection_work_on(self): collection.env = env record = mock.MagicMock(name='record') work = EventWorkContext(collection=collection, model_name='res.users', - from_recordset=record) + from_recordset=record, + components_registry=self.components_registry) work2 = work.work_on('res.partner') self.assertEquals('EventWorkContext', work2.__class__.__name__) self.assertEquals(collection, work2.collection) self.assertEquals(record, work2.from_recordset) self.assertEquals(env, work2.env) self.assertEquals('res.partner', work2.model_name) + self.assertEquals(self.components_registry, work2.components_registry) class TestEvent(ComponentRegistryCase): From bb7007fa61eadcc0e38dc7a83d118d8ac6ad1f88 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sun, 18 Jun 2017 21:36:22 +0200 Subject: [PATCH 03/80] Add cache in component_event So we don't need to iterate on the listeners to find the events each time the event is triggered. * rename producer -> collecter, which is more accurate regarding what it does * rename the trigger function 'fire' -> 'notify', just a matter of preference here :) * add a CollectedEvents class so we don't modify the state of the collecter and we don't mix responsibilities --- component_event/__manifest__.py | 3 + component_event/components/event.py | 65 ++++++++--- component_event/models/base.py | 4 +- component_event/tests/test_event.py | 168 ++++++++++++++++++++++++---- 4 files changed, 202 insertions(+), 38 deletions(-) diff --git a/component_event/__manifest__.py b/component_event/__manifest__.py index 169dbd11c..a2df6e633 100644 --- a/component_event/__manifest__.py +++ b/component_event/__manifest__.py @@ -11,6 +11,9 @@ 'category': 'Generic Modules', 'depends': ['component', ], + 'external_dependencies': { + 'python': ['cachetools'], + }, 'data': [], 'installable': True, } diff --git a/component_event/components/event.py b/component_event/components/event.py index 9496a6a7f..d5ff5956b 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -10,30 +10,70 @@ """ +import logging +import operator + 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, return the event methods +DEFAULT_EVENT_CACHE_SIZE = 128 + + +class CollectedEvents(object): + + def __init__(self, events): + self.events = events + + def notify(self, *args, **kwargs): + for event in self.events: + event(*args, **kwargs) -class EventProducer(AbstractComponent): - _name = 'base.event.producer' + +class EventCollecter(AbstractComponent): + _name = 'base.event.collecter' def __init__(self, work): - super(EventProducer, self).__init__(work) - self._events = set() + super(EventCollecter, self).__init__(work) - def collect_events(self, name): + @classmethod + def _complete_component_build(cls): + 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 + else None, + self.work.model_name, + name + )) + def _collect_events(self, name): + events = set([]) component_classes = self.work.components_registry.lookup( usage='event.listener', - model_name=self.model._name, + model_name=self.work.model_name, ) for cls in component_classes: if cls.has_event(name): component = cls(self.work) - self._events.add(getattr(component, name)) - return self + events.add(getattr(component, name)) + return events - def fire(self, *args, **kwargs): - for event in self._events: - event(*args, **kwargs) + def collect_events(self, name): + events = self._collect_events(name) + return CollectedEvents(events) class EventListener(AbstractComponent): @@ -60,9 +100,6 @@ def _build_event_listener_component(cls): if not cls._abstract: for attr_name in dir(cls): if attr_name.startswith('on_'): - # possible future optimization: store all events in a - # registry so we don't need to loop on event.listener - # components events.add(attr_name) cls._events = events diff --git a/component_event/models/base.py b/component_event/models/base.py index 7f277575d..3520e17ea 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -23,5 +23,5 @@ def _event(self, name, model_name=None, collection=None, from_recordset=self, components_registry=components_registry) - producer = work.component_by_name('base.event.producer') - return producer.collect_events(name) + collecter = work.component_by_name('base.event.collecter') + return collecter.collect_events(name) diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index a7d914ae5..95d2527ed 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -12,7 +12,7 @@ from odoo.addons.component.core import Component from odoo.addons.component_event.core import EventWorkContext from odoo.addons.component_event.components.event import ( - EventProducer, + EventCollecter, EventListener, ) @@ -108,18 +108,18 @@ def setUp(self): super(TestEvent, self).setUp() # build and push in the component registry the base components we # inherit from in the tests - # 'base.event.producer' - EventProducer._build_component(self.comp_registry) + # 'base.event.collecter' + EventCollecter._build_component(self.comp_registry) # 'base.event.listener' EventListener._build_component(self.comp_registry) - # get the producer to fire the event + # get the collecter to notify the event # we don't mind about the collection and the model here, # the events we test are global env = mock.MagicMock() work = EventWorkContext(model_name='res.users', env=env, components_registry=self.comp_registry) - self.producer = self.comp_registry['base.event.producer'](work) + self.collecter = self.comp_registry['base.event.collecter'](work) def test_event(self): class MyEventListener(Component): @@ -138,8 +138,8 @@ def on_record_create(self, recipient, something, fields=None): # modify this recipient to check it has been called recipient = [] - # collect the event and fire it - self.producer.collect_events('on_record_create').fire( + # collect the event and notify it + self.collecter.collect_events('on_record_create').notify( recipient, something, fields=fields ) self.assertEquals([('OK', something, fields)], recipient) @@ -169,14 +169,138 @@ def on_record_create(self, recipient, something, fields=None): # modify this recipient to check it has been called recipient = [] - # collect the event and fire them - events = self.producer.collect_events('on_record_create') - self.assertEquals(2, len(self.producer._events)) + # collect the event and notify them + collected = self.collecter.collect_events('on_record_create') + self.assertEquals(2, len(collected.events)) - events.fire(recipient, something, fields=fields) + collected.notify(recipient, something, fields=fields) self.assertEquals([('OK', something, fields), ('OK', something, fields)], recipient) + def test_event_cache(self): + class MyEventListener(Component): + _name = 'my.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self): + pass + + MyEventListener._build_component(self.comp_registry) + + # collect the event + collected = self.collecter.collect_events('on_record_create') + # CollectedEvents.events contains the collected events + self.assertEquals(1, len(collected.events)) + + # build and register a new listener + class MyOtherEventListener(Component): + _name = 'my.other.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self): + pass + + MyOtherEventListener._build_component(self.comp_registry) + + # get a new collecter and check that we it finds the same + # events even if we built a new one: it means the cache works + env = mock.MagicMock() + work = EventWorkContext(model_name='res.users', env=env, + components_registry=self.comp_registry) + collecter = self.comp_registry['base.event.collecter'](work) + collected = collecter.collect_events('on_record_create') + # CollectedEvents.events contains the collected events + self.assertEquals(1, len(collected.events)) + + # if we empty the cache, as it on the class, both collecters + # should now find the 2 events + collecter._cache.clear() + # CollectedEvents.events contains the collected events + self.assertEquals( + 2, + len(collecter.collect_events('on_record_create').events) + ) + self.assertEquals( + 2, + len(self.collecter.collect_events('on_record_create').events) + ) + + def test_event_cache_collection(self): + class MyEventListener(Component): + _name = 'my.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self): + pass + + MyEventListener._build_component(self.comp_registry) + + # collect the event + collected = self.collecter.collect_events('on_record_create') + # CollectedEvents.events contains the collected events + self.assertEquals(1, len(collected.events)) + + # build and register a new listener + class MyOtherEventListener(Component): + _name = 'my.other.event.listener' + _inherit = 'base.event.listener' + _collection = 'base.collection' + + def on_record_create(self): + pass + + MyOtherEventListener._build_component(self.comp_registry) + + # get a new collecter and check that we it finds the same + # events even if we built a new one: it means the cache works + collection = mock.MagicMock(name='base.collection') + collection._name = 'base.collection' + collection.env = mock.MagicMock() + work = EventWorkContext(model_name='res.users', collection=collection, + components_registry=self.comp_registry) + collecter = self.comp_registry['base.event.collecter'](work) + collected = collecter.collect_events('on_record_create') + # for a different collection, we should not have the same + # cache entry + self.assertEquals(2, len(collected.events)) + + def test_event_cache_model_name(self): + class MyEventListener(Component): + _name = 'my.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self): + pass + + MyEventListener._build_component(self.comp_registry) + + # collect the event + collected = self.collecter.collect_events('on_record_create') + # CollectedEvents.events contains the collected events + self.assertEquals(1, len(collected.events)) + + # build and register a new listener + class MyOtherEventListener(Component): + _name = 'my.other.event.listener' + _inherit = 'base.event.listener' + _apply_on = ['res.country'] + + def on_record_create(self): + pass + + MyOtherEventListener._build_component(self.comp_registry) + + # get a new collecter and check that we it finds the same + # events even if we built a new one: it means the cache works + env = mock.MagicMock() + work = EventWorkContext(model_name='res.country', env=env, + components_registry=self.comp_registry) + collecter = self.comp_registry['base.event.collecter'](work) + collected = collecter.collect_events('on_record_create') + # for a different collection, we should not have the same + # cache entry + self.assertEquals(2, len(collected.events)) + class TestEventRecordset(ComponentRegistryCase): """ Test Events with Recordset """ @@ -185,12 +309,12 @@ def setUp(self): super(TestEventRecordset, self).setUp() # build and push in the component registry the base components we # inherit from in the tests - # 'base.event.producer' - EventProducer._build_component(self.comp_registry) + # 'base.event.collecter' + EventCollecter._build_component(self.comp_registry) # 'base.event.listener' EventListener._build_component(self.comp_registry) - # get the producer to fire the event + # get the collecter to notify the event # we don't mind about the collection and the model here, # the events we test are global env = mock.MagicMock() @@ -202,7 +326,7 @@ def setUp(self): work = EventWorkContext(model_name='res.users', env=env, from_recordset=self.recordset, components_registry=self.comp_registry) - self.producer = self.comp_registry['base.event.producer'](work) + self.collecter = self.comp_registry['base.event.collecter'](work) def test_event(self): class MyEventListener(Component): @@ -214,8 +338,8 @@ def on_foo(self, msg): MyEventListener._build_component(self.comp_registry) - # collect the event and fire it - self.producer.collect_events('on_foo').fire('OK') + # collect the event and notify it + self.collecter.collect_events('on_foo').notify('OK') self.assertEquals('OK', self.recordset.msg) @@ -226,8 +350,8 @@ def setUp(self): super(TestEventFromModel, self).setUp() # build and push in the component registry the base components we # inherit from in the tests - # 'base.event.producer' - EventProducer._build_component(self.comp_registry) + # 'base.event.collecter' + EventCollecter._build_component(self.comp_registry) # 'base.event.listener' EventListener._build_component(self.comp_registry) @@ -246,10 +370,10 @@ def on_foo(self, name): # this is for the sake of the test, letting it empty # will use the global registry. # In a real code it would look like: - # partner._event('on_foo').fire('bar') + # partner._event('on_foo').notify('bar') events = partner._event('on_foo', components_registry=self.comp_registry) - events.fire('bar') + events.notify('bar') self.assertEquals('bar', partner.name) def test_event_filter_on_model(self): @@ -280,7 +404,7 @@ def on_foo(self, name): partner = self.env['res.partner'].create({'name': 'test'}) partner._event('on_foo', - components_registry=self.comp_registry).fire('bar') + components_registry=self.comp_registry).notify('bar') self.assertEquals('bar', partner.name) self.assertEquals('bar', partner.ref) From ffa67b98ac907dee55d8c160a23c35e2e85222c0 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 19 Jun 2017 11:48:55 +0200 Subject: [PATCH 04/80] Complete component_event * Add documentation * Add a few more test cases * Prevent using component lookup methods from an EventWorkContext when there is no collection, but allow to switch to a WorkContext with collection * Increase cache size (should be measured at some point...) * Fix a few issues with collection being an empty model being False-ish, so added comparisons with None * Add abstract Component with the base events (on_record_create, on_record_write, on_record_unlink) --- component_event/components/event.py | 217 ++++++++++++++++++++++------ component_event/core.py | 104 ++++++++++++- component_event/models/base.py | 82 ++++++++++- component_event/tests/test_event.py | 136 ++++++++--------- 4 files changed, 415 insertions(+), 124 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index d5ff5956b..8282e21ab 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -6,7 +6,113 @@ Events ====== -TODO +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. + +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. + +When implementing generic events, it is a good practice to create a +base abstract component implementing empty methods for those +events, such as:: + + class RecordsEventListener(AbstractComponent): + _name = 'records.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self, record, fields=None): + \"\"\" Called when a record is created \"\"\" + +It can serve for inheritance and is a documentation of the 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) + """ @@ -23,28 +129,66 @@ _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, return the event methods -DEFAULT_EVENT_CACHE_SIZE = 128 +# 1 item means: for an event name, model_name, collection, return +# the event methods +DEFAULT_EVENT_CACHE_SIZE = 512 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(AbstractComponent): - _name = 'base.event.collecter' +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:`..core.EventWorkContext` class should be + used for this Component, because it can work + without a collection. - def __init__(self, work): - super(EventCollecter, self).__init__(work) + It is used by :meth:`..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 @@ -54,14 +198,18 @@ def _complete_component_build(cls): @cachedmethod(operator.attrgetter('_cache'), key=lambda self, name: keys.hashkey( - self.work.collection._name if self.work._collection - else None, + self.work.collection._name + if self.work._collection is not None else None, self.work.model_name, name )) def _collect_events(self, name): events = 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, ) @@ -72,6 +220,10 @@ def _collect_events(self, 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._collect_events(name) return CollectedEvents(events) @@ -83,19 +235,18 @@ class EventListener(AbstractComponent): Example: :class:`RecordsEventListener` - Inside an event method, you can access to the record or records that - triggered the event using ``self.recordset``. - """ _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): @@ -108,43 +259,17 @@ def _complete_component_build(cls): super(EventListener, cls)._complete_component_build() cls._build_event_listener_component() - @property - def recordset(self): - """ Recordset that triggered the event """ - return getattr(self.work, 'from_recordset', None) - - # TODO: error if we don't have the collection - def component_by_name(self, name, model_name=None): - raise NotImplementedError - - def component(self, usage=None, model_name=None): - raise NotImplementedError - - def many_components(self, usage=None, model_name=None): - raise NotImplementedError - -class RecordsEventListener(Component): +class RecordsEventListener(AbstractComponent): + """ Abstract Component with base events """ _name = 'records.event.listener' _inherit = 'base.event.listener' - def on_record_create(self, fields=None): - """ Called when a record is created - - The record that triggered the event is in ``self.record``. - - """ - - def on_record_write(self, fields=None): - """ Called when a record is modified - - The record that triggered the event is in ``self.record``. - - """ - - def on_record_unlink(self, fields=None): - """ Called when a record is deleted + def on_record_create(self, record, fields=None): + """ Called when a record is created """ - The record that triggered the event is in ``self.record``. + def on_record_write(self, record, fields=None): + """ Called when a record is modified """ - """ + def on_record_unlink(self, record): + """ Called when a record is deleted """ diff --git a/component_event/core.py b/component_event/core.py index 9b5e32b9b..fd76375ec 100644 --- a/component_event/core.py +++ b/component_event/core.py @@ -2,19 +2,44 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +""" +Events Internals +================ + +Core classes for the events system. + + +""" + from odoo.addons.component.core import WorkContext class EventWorkContext(WorkContext): - """ TODO + """ Work context used by the Events internals + + Should not be used outside of the events internals. + The work context to use generally is + :class:`odoo.addons.component.core.WorkContext` or your own + subclass. + + The events are a special kind of components because they are + not attached to any collection (they can but not the main use case). + + So the work context must not need to have a collection, but when + it has no collection, it must at least have an 'env'. + + When no collection is provided, the methods to get the Components + cannot be used, but :meth:`work_on` can be used to switch back to + a :class:`odoo.addons.component.core.WorkContext` with collection. + This is needed when one want to get a component for a collection + from inside an event listener. """ def __init__(self, model_name=None, collection=None, env=None, - from_recordset=None, components_registry=None, **kwargs): - if not (collection or env): + if not (collection is not None or env): raise ValueError('collection or env is required') if collection and env: # when a collection is used, the env will be the one of @@ -25,7 +50,6 @@ def __init__(self, model_name=None, collection=None, env=None, super(EventWorkContext, self).__init__( model_name=model_name, collection=collection, components_registry=components_registry, - from_recordset=from_recordset, **kwargs ) if self._env: @@ -46,7 +70,7 @@ def env(self, value): @property def collection(self): """ Return the current Odoo env """ - if self._collection: + if self._collection is not None: return self._collection raise ValueError('No collection, it is optional for EventWorkContext') @@ -54,6 +78,76 @@ def collection(self): def collection(self, value): self._collection = value + 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. + + Used on an EventWorkContext, it switch back to a normal + WorkContext. It means we are inside an event listener, and + we want to get a component. We need to set a collection + to be able to get components. + """ + if self._collection is None and collection is None: + raise ValueError('you must provide a collection to work with') + if collection is not None: + if self.env is not collection.env: + raise ValueError('the Odoo env of the collection must be ' + 'the same than the current one') + kwargs = {attr_name: getattr(self, attr_name) + for attr_name in self._propagate_kwargs} + kwargs.pop('env', None) + if collection is not None: + kwargs['collection'] = collection + if model_name is not None: + kwargs['model_name'] = model_name + return WorkContext(**kwargs) + + def component_by_name(self, name, model_name=None): + if self._collection is not None: + # switch to a normal WorkContext + work = self.work_on(collection=self._collection, + model_name=model_name) + else: + raise TypeError( + "Can't be used on an EventWorkContext without collection. " + "The collection must be known to find components.\n" + "Hint: you can set the collection and get a component with:\n" + ">>> work.work_on(collection=self.env[...].browse(...))\n" + ">>> work.component_by_name(name, model_name=model_name)" + ) + return work.component_by_name(name, model_name=model_name) + + def component(self, usage=None, model_name=None): + if self._collection is not None: + # switch to a normal WorkContext + work = self.work_on(collection=self._collection, + model_name=model_name) + else: + raise TypeError( + "Can't be used on an EventWorkContext without collection. " + "The collection must be known to find components.\n" + "Hint: you can set the collection and get a component with:\n" + ">>> work.work_on(collection=self.env[...].browse(...))\n" + ">>> work.component(usage=usage, model_name=model_name)" + ) + return work.component(usage=usage, model_name=model_name) + + def many_components(self, usage=None, model_name=None): + if self._collection is not None: + # switch to a normal WorkContext + work = self.work_on(collection=self._collection, + model_name=model_name) + else: + raise TypeError( + "Can't be used on an EventWorkContext without collection. " + "The collection must be known to find components.\n" + "Hint: you can set the collection and get a component with:\n" + ">>> work.work_on(collection=self.env[...].browse(...))\n" + ">>> work.many_components(usage=usage, model_name=model_name)" + ) + return work.component(usage=usage, model_name=model_name) + def __str__(self): return ("EventWorkContext(%s,%s)" % (repr(self._env or self._collection), self.model_name)) diff --git a/component_event/models/base.py b/component_event/models/base.py index 3520e17ea..747443371 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -2,26 +2,94 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from odoo import models +""" +Base Model +========== + +Extend the 'base' Odoo Model to add Events related features. + + +""" + +from odoo import api, models from ..core import EventWorkContext class Base(models.AbstractModel): - """ The base model, which is implicitly inherited by all models. """ + """ The base model, which is implicitly inherited by all models. + + Add an :meth:`_event` method to all Models. This method allows to + trigger events. + + It also notifies the following events: + + * ``on_record_create`` + * ``on_record_multi`` + * ``on_record_unlink`` + + + """ _inherit = 'base' def _event(self, name, model_name=None, collection=None, components_registry=None): - model_name = model_name or self._name - if collection: + """ Collect events for notifications + + Usage:: + + @api.multi + def button_do_something(self): + for record in self: + # do something + self._event('on_do_something').notify('something') + + With this line, every listener having a ``on_do_something`` method + with be called and receive 'something' as argument. + + See: :mod:`..components.event` + + :param name: name of the event, start with 'on_' + :type model_name: str | unicode + :param collection: optional collection to filter on, only + listeners with similar ``_collection`` will be + notified + :type model_name: :class:`odoo.models.BaseModel` + :param components_registry: component registry for lookups, + mainly used for tests + :type components_registry: + :class:`odoo.addons.components.core.ComponentRegistry` + + + """ + model_name = self._name + if collection is not None: work = EventWorkContext(collection=collection, model_name=model_name, - from_recordset=self, components_registry=components_registry) else: work = EventWorkContext(env=self.env, model_name=model_name, - from_recordset=self, components_registry=components_registry) - collecter = work.component_by_name('base.event.collecter') + collecter = work._component_class_by_name('base.event.collecter')(work) return collecter.collect_events(name) + + @api.model + def create(self, vals): + record = super(Base, self).create(vals) + self._event('on_record_create').notify(record, fields=vals.keys()) + return record + + @api.multi + def write(self, vals): + result = super(Base, self).write(vals) + fields = vals.keys() + for record in self: + self._event('on_record_write').notify(record, fields=fields) + return result + + @api.multi + def unlink(self): + result = super(Base, self).unlink() + for record in self: + self._event('on_record_unlink').notify(record) + return result diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index 95d2527ed..c00f04dd5 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -29,11 +29,9 @@ def setUp(self): def test_env(self): """ WorkContext with env """ work = EventWorkContext(model_name='res.users', env=self.env, - from_recordset=self.record, components_registry=self.components_registry) self.assertEquals(self.env, work.env) self.assertEquals('res.users', work.model_name) - self.assertEquals(self.record, work.from_recordset) with self.assertRaises(ValueError): work.collection @@ -42,12 +40,9 @@ def test_collection(self): env = mock.MagicMock(name='env') collection = mock.MagicMock(name='collection') collection.env = env - record = mock.MagicMock(name='record') work = EventWorkContext(model_name='res.users', collection=collection, - from_recordset=record, components_registry=self.components_registry) self.assertEquals(collection, work.collection) - self.assertEquals(record, work.from_recordset) self.assertEquals(env, work.env) self.assertEquals('res.users', work.model_name) @@ -70,14 +65,13 @@ def test_missing(self): def test_env_work_on(self): """ WorkContext propagated through work_on """ env = mock.MagicMock(name='env') - record = mock.MagicMock(name='record') + collection = mock.MagicMock(name='collection') + collection.env = env work = EventWorkContext(env=env, model_name='res.users', - from_recordset=record, components_registry=self.components_registry) - work2 = work.work_on('res.partner') - self.assertEquals('EventWorkContext', work2.__class__.__name__) + work2 = work.work_on(model_name='res.partner', collection=collection) + self.assertEquals('WorkContext', work2.__class__.__name__) self.assertEquals(env, work2.env) - self.assertEquals(record, work2.from_recordset) self.assertEquals('res.partner', work2.model_name) self.assertEquals(self.components_registry, work2.components_registry) with self.assertRaises(ValueError): @@ -88,18 +82,32 @@ def test_collection_work_on(self): env = mock.MagicMock(name='env') collection = mock.MagicMock(name='collection') collection.env = env - record = mock.MagicMock(name='record') work = EventWorkContext(collection=collection, model_name='res.users', - from_recordset=record, components_registry=self.components_registry) - work2 = work.work_on('res.partner') - self.assertEquals('EventWorkContext', work2.__class__.__name__) + work2 = work.work_on(model_name='res.partner') + self.assertEquals('WorkContext', work2.__class__.__name__) self.assertEquals(collection, work2.collection) - self.assertEquals(record, work2.from_recordset) self.assertEquals(env, work2.env) self.assertEquals('res.partner', work2.model_name) self.assertEquals(self.components_registry, work2.components_registry) + def test_collection_work_on_collection(self): + """ WorkContext collection changed with work_on """ + env = mock.MagicMock(name='env') + collection = mock.MagicMock(name='collection') + collection.env = env + work = EventWorkContext(model_name='res.users', env=env, + components_registry=self.components_registry) + work2 = work.work_on(collection=collection) + # when work_on is used inside an event component, we want + # to switch back to a normal WorkContext, because we don't + # need anymore the EventWorkContext + self.assertEquals('WorkContext', work2.__class__.__name__) + self.assertEquals(collection, work2.collection) + self.assertEquals(env, work2.env) + self.assertEquals('res.users', work2.model_name) + self.assertEquals(self.components_registry, work2.components_registry) + class TestEvent(ComponentRegistryCase): """ Test Events Components """ @@ -215,6 +223,7 @@ def on_record_create(self): # if we empty the cache, as it on the class, both collecters # should now find the 2 events collecter._cache.clear() + self.comp_registry._cache.clear() # CollectedEvents.events contains the collected events self.assertEquals( 2, @@ -302,47 +311,6 @@ def on_record_create(self): self.assertEquals(2, len(collected.events)) -class TestEventRecordset(ComponentRegistryCase): - """ Test Events with Recordset """ - - def setUp(self): - super(TestEventRecordset, self).setUp() - # build and push in the component registry the base components we - # inherit from in the tests - # 'base.event.collecter' - EventCollecter._build_component(self.comp_registry) - # 'base.event.listener' - EventListener._build_component(self.comp_registry) - - # get the collecter to notify the event - # we don't mind about the collection and the model here, - # the events we test are global - env = mock.MagicMock() - self.recordset = mock.MagicMock(name='recordset') - # when there is a 'from_recordset' in the WorkContext, - # the listener methods will be able to access to it from - # self.recordset. It is used when events are triggered from - # records using BaseModel._event - work = EventWorkContext(model_name='res.users', env=env, - from_recordset=self.recordset, - components_registry=self.comp_registry) - self.collecter = self.comp_registry['base.event.collecter'](work) - - def test_event(self): - class MyEventListener(Component): - _name = 'my.event.listener' - _inherit = 'base.event.listener' - - def on_foo(self, msg): - self.recordset.msg = msg - - MyEventListener._build_component(self.comp_registry) - - # collect the event and notify it - self.collecter.collect_events('on_foo').notify('OK') - self.assertEquals('OK', self.recordset.msg) - - class TestEventFromModel(TransactionComponentRegistryCase): """ Test Events Components from Models """ @@ -360,8 +328,8 @@ class MyEventListener(Component): _name = 'my.event.listener' _inherit = 'base.event.listener' - def on_foo(self, name): - self.recordset.name = name + def on_foo(self, record, name): + record.name = name MyEventListener._build_component(self.comp_registry) @@ -373,7 +341,7 @@ def on_foo(self, name): # partner._event('on_foo').notify('bar') events = partner._event('on_foo', components_registry=self.comp_registry) - events.notify('bar') + events.notify(partner, 'bar') self.assertEquals('bar', partner.name) def test_event_filter_on_model(self): @@ -381,30 +349,66 @@ class GlobalListener(Component): _name = 'global.event.listener' _inherit = 'base.event.listener' - def on_foo(self, name): - self.recordset.name = name + def on_foo(self, record, name): + record.name = name class PartnerListener(Component): _name = 'partner.event.listener' _inherit = 'base.event.listener' _apply_on = ['res.partner'] - def on_foo(self, name): - self.recordset.ref = name + def on_foo(self, record, name): + record.ref = name class UserListener(Component): _name = 'user.event.listener' _inherit = 'base.event.listener' _apply_on = ['res.users'] - def on_foo(self, name): + def on_foo(self, record, name): assert False self._build_components(GlobalListener, PartnerListener, UserListener) partner = self.env['res.partner'].create({'name': 'test'}) - partner._event('on_foo', - components_registry=self.comp_registry).notify('bar') + partner._event( + 'on_foo', + components_registry=self.comp_registry + ).notify(partner, 'bar') self.assertEquals('bar', partner.name) self.assertEquals('bar', partner.ref) + def test_event_filter_on_collection(self): + class GlobalListener(Component): + _name = 'global.event.listener' + _inherit = 'base.event.listener' + + def on_foo(self, record, name): + record.name = name + + class PartnerListener(Component): + _name = 'partner.event.listener' + _inherit = 'base.event.listener' + _collection = 'collection.base' + + def on_foo(self, record, name): + record.ref = name + + class UserListener(Component): + _name = 'user.event.listener' + _inherit = 'base.event.listener' + _collection = 'magento.backend' + + def on_foo(self, record, name): + assert False + + self._build_components(GlobalListener, PartnerListener, UserListener) + + partner = self.env['res.partner'].create({'name': 'test'}) + events = partner._event( + 'on_foo', collection=self.env['collection.base'], + components_registry=self.comp_registry + ) + events.notify(partner, 'bar') + self.assertEquals('bar', partner.name) + self.assertEquals('bar', partner.ref) From c2ff298f07bfeaa4a7b8d9dd97f585bf439bdde6 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 19 Jun 2017 14:42:14 +0200 Subject: [PATCH 05/80] Prevent trigger of events before registry is ready We return an empty list of events so nothing is triggered --- component_event/models/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/component_event/models/base.py b/component_event/models/base.py index 747443371..f7384f3f2 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -12,6 +12,7 @@ """ from odoo import api, models +from ..components.event import CollectedEvents from ..core import EventWorkContext @@ -61,6 +62,9 @@ def button_do_something(self): """ + if not self.env.registry.ready: + # no event should be triggered before the registry has been loaded + return CollectedEvents([]) model_name = self._name if collection is not None: work = EventWorkContext(collection=collection, From d0bfdff413ed62dd0346bc152ec1ed6b88ec509a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 19 Jun 2017 17:14:11 +0200 Subject: [PATCH 06/80] Fix test: post_install Tests using odoo transactions must run post install, because during the install the registry is not ready, so the components aren't neither. --- component_event/__manifest__.py | 1 - component_event/tests/test_event.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/component_event/__manifest__.py b/component_event/__manifest__.py index a2df6e633..fe2659e25 100644 --- a/component_event/__manifest__.py +++ b/component_event/__manifest__.py @@ -17,4 +17,3 @@ 'data': [], 'installable': True, } - diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index c00f04dd5..82bf920cf 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -33,7 +33,7 @@ def test_env(self): self.assertEquals(self.env, work.env) self.assertEquals('res.users', work.model_name) with self.assertRaises(ValueError): - work.collection + work.collection # noqa def test_collection(self): """ WorkContext with collection """ @@ -75,7 +75,7 @@ def test_env_work_on(self): self.assertEquals('res.partner', work2.model_name) self.assertEquals(self.components_registry, work2.components_registry) with self.assertRaises(ValueError): - work.collection + work.collection # noqa def test_collection_work_on(self): """ WorkContext propagated through work_on """ @@ -314,6 +314,9 @@ def on_record_create(self): class TestEventFromModel(TransactionComponentRegistryCase): """ Test Events Components from Models """ + at_install = False + post_install = True + def setUp(self): super(TestEventFromModel, self).setUp() # build and push in the component registry the base components we From 232b4b4182a6b7e78c166e5ab21cd9a0ce4f7ee3 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 20 Jun 2017 09:35:09 +0200 Subject: [PATCH 07/80] Move base component in a components directory For consistency, this is where components should go (as for models, views, ...) --- component_event/components/event.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index 8282e21ab..036e80b64 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -201,8 +201,8 @@ def _complete_component_build(cls): self.work.collection._name if self.work._collection is not None else None, self.work.model_name, - name - )) + name) + ) def _collect_events(self, name): events = set([]) collection_name = (self.work.collection._name From 5c30b0a4e1e211bf26a3c1db17da7ce2c5c933ea Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 20 Jun 2017 09:58:33 +0200 Subject: [PATCH 08/80] Check that component registry is ready for events Checking that the odoo registry is ready is not working: in tests, the events are not working because they are run just before the odoo registry is set to ready. --- component_event/models/base.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/component_event/models/base.py b/component_event/models/base.py index f7384f3f2..5aa92f0b6 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -12,6 +12,7 @@ """ from odoo import api, models +from odoo.addons.component.core import _component_databases from ..components.event import CollectedEvents from ..core import EventWorkContext @@ -62,9 +63,21 @@ def button_do_something(self): """ - if not self.env.registry.ready: - # no event should be triggered before the registry has been loaded + dbname = self.env.cr.dbname + comp_registry = ( + components_registry or _component_databases.get(dbname) + ) + if not comp_registry or not comp_registry.ready: + # No event should be triggered before the registry has been loaded + # This is a very special case, when the odoo registry is being + # built, it calls odoo.modules.loading.load_modules(). + # This function might trigger events (by writing on records, ...). + # But at this point, the component registry is not guaranteed + # to be ready, and anyway we should probably not trigger events + # during the initialization. Hence we return an empty list of + # events, the 'notify' calls will do nothing. return CollectedEvents([]) + model_name = self._name if collection is not None: work = EventWorkContext(collection=collection, From fdc83ef03a981da37fead4b448556f9ccf85e709 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 20 Jun 2017 10:24:44 +0200 Subject: [PATCH 09/80] Ignore W0104 pylint warnings in tests --- component_event/tests/test_event.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index 82bf920cf..2a6d3f298 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -33,6 +33,7 @@ def test_env(self): self.assertEquals(self.env, work.env) self.assertEquals('res.users', work.model_name) with self.assertRaises(ValueError): + # pylint: disable=W0104 work.collection # noqa def test_collection(self): @@ -75,6 +76,7 @@ def test_env_work_on(self): self.assertEquals('res.partner', work2.model_name) self.assertEquals(self.components_registry, work2.components_registry) with self.assertRaises(ValueError): + # pylint: disable=W0104 work.collection # noqa def test_collection_work_on(self): From ff9e3ed9479a6e4e2b1e99c45f1003346c65392f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 20 Jun 2017 10:42:31 +0200 Subject: [PATCH 10/80] Remove doc about creating abstract listener I'm not sure it is good actually. If you inherit from it for only one of the method, the other methods will listen be executed for doing nothing, but that's still a useless call of method... --- component_event/components/event.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index 036e80b64..2bb3f335f 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -64,19 +64,6 @@ def on_record_create(self, record, fields=None): Many listeners such as this one could be added for the same event. -When implementing generic events, it is a good practice to create a -base abstract component implementing empty methods for those -events, such as:: - - class RecordsEventListener(AbstractComponent): - _name = 'records.event.listener' - _inherit = 'base.event.listener' - - def on_record_create(self, record, fields=None): - \"\"\" Called when a record is created \"\"\" - -It can serve for inheritance and is a documentation of the event. - Collection and models --------------------- From a432eecfc71404b4da9b954c71aed466fe1bb13f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 21 Jun 2017 11:20:35 +0200 Subject: [PATCH 11/80] Correct event name in doc and add signature --- component_event/models/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/component_event/models/base.py b/component_event/models/base.py index 5aa92f0b6..b6a859279 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -25,10 +25,9 @@ class Base(models.AbstractModel): It also notifies the following events: - * ``on_record_create`` - * ``on_record_multi`` - * ``on_record_unlink`` - + * ``on_record_create(self, record, fields=None)`` + * ``on_record_write(self, record, fields=none)`` + * ``on_record_unlink(self, record)`` """ _inherit = 'base' From 4035aac2213d02fde25cdcbd33ce61566ee80b0e Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 21 Jun 2017 16:42:57 +0200 Subject: [PATCH 12/80] Remove RecordsEventListener It's not really usable: if we inherit from it and implement only one method, the others will be called without effect, waste of resources --- component_event/components/event.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index 2bb3f335f..8e673d262 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -245,18 +245,3 @@ def _build_event_listener_component(cls): def _complete_component_build(cls): super(EventListener, cls)._complete_component_build() cls._build_event_listener_component() - - -class RecordsEventListener(AbstractComponent): - """ Abstract Component with base events """ - _name = 'records.event.listener' - _inherit = 'base.event.listener' - - def on_record_create(self, record, fields=None): - """ Called when a record is created """ - - def on_record_write(self, record, fields=None): - """ Called when a record is modified """ - - def on_record_unlink(self, record): - """ Called when a record is deleted """ From 79be7789ff200b051ccc5232d10de35d97420c22 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 22 Jun 2017 08:56:33 +0200 Subject: [PATCH 13/80] Add @skip_if decorator to skip events It takes a condition which skip the event when evaluated to True. A new base listener for the connector adds a default condition based on self.env.context.get('connector_no_export'). Every export event listener should be decorated with: @skip_if(lambda self, *args, **kwargs: self.no_connector_export) --- component_event/__init__.py | 3 +++ component_event/components/event.py | 42 +++++++++++++++++++++++++++-- component_event/tests/test_event.py | 26 +++++++++++++++++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/component_event/__init__.py b/component_event/__init__.py index 6a0f974d1..47e8136cd 100644 --- a/component_event/__init__.py +++ b/component_event/__init__.py @@ -2,3 +2,6 @@ from . import core from . import components from . import models + +# allow public API 'from odoo.addons.component_event import skip_if' +from .components.event import skip_if # noqa diff --git a/component_event/components/event.py b/component_event/components/event.py index 8e673d262..e96ec0c08 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -100,12 +100,17 @@ def on_record_create(self, record, fields=None): 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 functools import wraps + from odoo.addons.component.core import AbstractComponent, Component _logger = logging.getLogger(__name__) @@ -121,6 +126,39 @@ def on_record_create(self, record, fields=None): 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 @@ -164,11 +202,11 @@ class EventCollecter(Component): An event always starts with ``on_``. Note that the special - :class:`..core.EventWorkContext` class should be + :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:`..models.base.Base._event`. + It is used by :meth:`odoo.addons.component_event.models.base.Base._event`. """ _name = 'base.event.collecter' diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index 2a6d3f298..e95d52a83 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2013-2017 Camptocamp SA +# Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import mock @@ -14,6 +14,7 @@ from odoo.addons.component_event.components.event import ( EventCollecter, EventListener, + skip_if, ) @@ -312,6 +313,29 @@ def on_record_create(self): # cache entry self.assertEquals(2, len(collected.events)) + def test_skip_if(self): + class MyEventListener(Component): + _name = 'my.event.listener' + _inherit = 'base.event.listener' + + def on_record_create(self, msg): + assert True + + class MyOtherEventListener(Component): + _name = 'my.other.event.listener' + _inherit = 'base.event.listener' + + @skip_if(lambda self, msg: msg == 'foo') + def on_record_create(self, msg): + assert False + + self._build_components(MyEventListener, MyOtherEventListener) + + # collect the event and notify it + collected = self.collecter.collect_events('on_record_create') + self.assertEquals(2, len(collected.events)) + collected.notify('foo') + class TestEventFromModel(TransactionComponentRegistryCase): """ Test Events Components from Models """ From bab0f194a1b1989384f2be2819ca0dd122764727 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 22 Jun 2017 22:17:13 +0200 Subject: [PATCH 14/80] Notify unlink event before the unlink Otherwise we can't read the record --- component_event/models/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/component_event/models/base.py b/component_event/models/base.py index b6a859279..b30bd20c9 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -29,6 +29,8 @@ class Base(models.AbstractModel): * ``on_record_write(self, record, fields=none)`` * ``on_record_unlink(self, record)`` + ``on_record_unlink`` is notified just *before* the unlink is done. + """ _inherit = 'base' @@ -105,7 +107,7 @@ def write(self, vals): @api.multi def unlink(self): - result = super(Base, self).unlink() for record in self: self._event('on_record_unlink').notify(record) + result = super(Base, self).unlink() return result From 1711bcf631bb18a0ee7e36df69c1ccb405aa1126 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 30 Jun 2017 11:02:29 +0200 Subject: [PATCH 15/80] Simplify tests by loading modules components It the previous commit, @lmignon added the possibility to load all components of an addon in a Component Registry. This commit takes benefit of this feature to simplify the existing tests and to add a base test case for the Connector (that loads all components of 'component', 'component_event', 'connector'). It can be used in implementations using the connector. --- component_event/tests/test_event.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index e95d52a83..f414a9893 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -11,11 +11,7 @@ ) from odoo.addons.component.core import Component from odoo.addons.component_event.core import EventWorkContext -from odoo.addons.component_event.components.event import ( - EventCollecter, - EventListener, - skip_if, -) +from odoo.addons.component_event.components.event import skip_if class TestEventWorkContext(unittest2.TestCase): @@ -117,12 +113,7 @@ class TestEvent(ComponentRegistryCase): def setUp(self): super(TestEvent, self).setUp() - # build and push in the component registry the base components we - # inherit from in the tests - # 'base.event.collecter' - EventCollecter._build_component(self.comp_registry) - # 'base.event.listener' - EventListener._build_component(self.comp_registry) + self._load_module_components('component_event') # get the collecter to notify the event # we don't mind about the collection and the model here, @@ -340,17 +331,9 @@ def on_record_create(self, msg): class TestEventFromModel(TransactionComponentRegistryCase): """ Test Events Components from Models """ - at_install = False - post_install = True - def setUp(self): super(TestEventFromModel, self).setUp() - # build and push in the component registry the base components we - # inherit from in the tests - # 'base.event.collecter' - EventCollecter._build_component(self.comp_registry) - # 'base.event.listener' - EventListener._build_component(self.comp_registry) + self._load_module_components('component_event') def test_event_from_model(self): class MyEventListener(Component): From 1cecf333e09699f60133574e9bb82a5f5c85b612 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 30 Jun 2017 17:30:59 +0200 Subject: [PATCH 16/80] Remove model_name in _event() It is unused, and depends on the model on which we call it --- component_event/models/base.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/component_event/models/base.py b/component_event/models/base.py index b30bd20c9..d2ca468a5 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -34,8 +34,7 @@ class Base(models.AbstractModel): """ _inherit = 'base' - def _event(self, name, model_name=None, collection=None, - components_registry=None): + def _event(self, name, collection=None, components_registry=None): """ Collect events for notifications Usage:: @@ -52,11 +51,9 @@ def button_do_something(self): See: :mod:`..components.event` :param name: name of the event, start with 'on_' - :type model_name: str | unicode :param collection: optional collection to filter on, only listeners with similar ``_collection`` will be notified - :type model_name: :class:`odoo.models.BaseModel` :param components_registry: component registry for lookups, mainly used for tests :type components_registry: From 93c22e780f0c4def0c3b4164a08964d47c9fc28c Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 11 Jul 2017 15:18:51 +0200 Subject: [PATCH 17/80] Add readme files --- component_event/README.rst | 108 +++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 component_event/README.rst diff --git a/component_event/README.rst b/component_event/README.rst new file mode 100644 index 000000000..56ecde7c0 --- /dev/null +++ b/component_event/README.rst @@ -0,0 +1,108 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +=================== +Components - Events +=================== + +This module implements an event system (`Observer pattern`_) and is a +base block for the Connector Framework. It can be used without +using the full Connector though. It is built upon the ``component`` module. + +Documentation: http://odoo-connector.com/ + +.. _Observer pattern: https://en.wikipedia.org/wiki/Observer_pattern + +Installation +============ + +* Install ``component_event`` + +Configuration +============= + +The module does nothing by itself and has no configuration. + +Usage +===== + +As a developer, you have access to a events system. You can find the +documentation in the code or on http://odoo-connector.com + +In a nutshell, you can create trigger events:: + + 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 + +And subscribe listeners to the events:: + + from odoo.addons.component.core import Component + from odoo.addons.component_event import skip_if + + class MagentoListener(Component): + _name = 'magento.event.listener' + _inherit = 'base.connector.listener' + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_create(self, record, fields=None): + """ Called when a record is created """ + record.with_delay().export_record(fields=fields) + + +This module triggers 3 events: + +* ``on_record_create(record, fields=None)`` +* ``on_record_write(record, fields=None)`` +* ``on_record_unlink(record)`` + + +Known issues / Roadmap +====================== + +* ... + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Guewen Baconnier + +Do not contact contributors directly about support or help with technical issues. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. + From bca4e0b154556a260083d52cd19e815b3de1ffc7 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 17 Jul 2017 18:14:54 +0200 Subject: [PATCH 18/80] Fix cache of events The cache entries for events were including instance of the components, hence, they included the work instance / odoo environment. Only the component class and the events it provides are new cached, the instances of components being instanciated when needed. Reported on: https://github.com/OCA/connector-magento/pull/255#issuecomment-315396030 --- component_event/components/event.py | 12 ++++++++++-- component_event/tests/test_event.py | 12 +++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index e96ec0c08..64cb3b349 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -109,6 +109,7 @@ def on_record_create(self, record, fields=None): import logging import operator +from collections import defaultdict from functools import wraps from odoo.addons.component.core import AbstractComponent, Component @@ -229,7 +230,7 @@ def _complete_component_build(cls): name) ) def _collect_events(self, name): - events = set([]) + events = defaultdict(set) collection_name = (self.work.collection._name if self.work._collection is not None else None) @@ -240,6 +241,13 @@ def _collect_events(self, 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.iteritems(): + for name in names: component = cls(self.work) events.add(getattr(component, name)) return events @@ -249,7 +257,7 @@ def collect_events(self, name): if not name.startswith('on_'): raise ValueError("an event name always starts with 'on_'") - events = self._collect_events(name) + events = self._init_collected_events(self._collect_events(name)) return CollectedEvents(events) diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index f414a9893..77370ee3b 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -119,9 +119,9 @@ def setUp(self): # we don't mind about the collection and the model here, # the events we test are global env = mock.MagicMock() - work = EventWorkContext(model_name='res.users', env=env, - components_registry=self.comp_registry) - self.collecter = self.comp_registry['base.event.collecter'](work) + self.work = EventWorkContext(model_name='res.users', env=env, + components_registry=self.comp_registry) + self.collecter = self.comp_registry['base.event.collecter'](self.work) def test_event(self): class MyEventListener(Component): @@ -193,6 +193,9 @@ def on_record_create(self): collected = self.collecter.collect_events('on_record_create') # CollectedEvents.events contains the collected events self.assertEquals(1, len(collected.events)) + event = list(collected.events)[0] + self.assertEquals(self.work, event.im_self.work) + self.assertEquals(self.work.env, event.im_self.work.env) # build and register a new listener class MyOtherEventListener(Component): @@ -213,6 +216,9 @@ def on_record_create(self): collected = collecter.collect_events('on_record_create') # CollectedEvents.events contains the collected events self.assertEquals(1, len(collected.events)) + event = list(collected.events)[0] + self.assertEquals(work, event.im_self.work) + self.assertEquals(env, event.im_self.work.env) # if we empty the cache, as it on the class, both collecters # should now find the 2 events From 216f466874f8aa70c3be60b17279a777a8c67131 Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Wed, 30 Aug 2017 10:57:56 -0700 Subject: [PATCH 19/80] [IMP] component_event: Document implemented events * Document the events that have already been implemented to BaseModel --- component_event/components/event.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/component_event/components/event.py b/component_event/components/event.py index 64cb3b349..611a43dd5 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -43,6 +43,12 @@ def create(self, vals): :meth:`CollectedEvents.notify` method triggers the event and forward the arguments to the listeners. +The following events have been implemented into the `'base'` model: + +* `on_record_create` - Listener receives args `record, fields` +* `on_record_write` - Listener receives args `record, fields` +* `on_record_unlink` - Listener receives args `record` + This should be done only once. Listeners From 16561dc6290bab8ad80b8a622d29fc4e97bd88b5 Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Wed, 30 Aug 2017 11:23:12 -0700 Subject: [PATCH 20/80] [FIX] component_event: Link to pre-existing docs for model events --- component_event/components/event.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index 611a43dd5..ac6ab1533 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -43,13 +43,8 @@ def create(self, vals): :meth:`CollectedEvents.notify` method triggers the event and forward the arguments to the listeners. -The following events have been implemented into the `'base'` model: - -* `on_record_create` - Listener receives args `record, fields` -* `on_record_write` - Listener receives args `record, fields` -* `on_record_unlink` - Listener receives args `record` - -This should be done only once. +This should be done only once. See :class:`..models.base.Base` for a list of +events that are implemented in the `'base'` model. Listeners --------- From ac0d23a0a14214efeaef0281bb58fd5a14e512df Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Oct 2017 10:16:49 +0200 Subject: [PATCH 21/80] [MIG] Set modules uninstallable --- component_event/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component_event/__manifest__.py b/component_event/__manifest__.py index fe2659e25..08fb664f5 100644 --- a/component_event/__manifest__.py +++ b/component_event/__manifest__.py @@ -15,5 +15,5 @@ 'python': ['cachetools'], }, 'data': [], - 'installable': True, + 'installable': False, } From 167fb5c1f415ead6d7e64e38674423bf6a0eb962 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Oct 2017 10:37:57 +0200 Subject: [PATCH 22/80] Make addons installable --- component_event/__manifest__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component_event/__manifest__.py b/component_event/__manifest__.py index 08fb664f5..86c3a1d91 100644 --- a/component_event/__manifest__.py +++ b/component_event/__manifest__.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) {'name': 'Components Events', - 'version': '10.0.1.0.0', + 'version': '11.0.1.0.0', 'author': 'Camptocamp,' 'Odoo Community Association (OCA)', 'website': 'https://www.camptocamp.com', @@ -15,5 +15,5 @@ 'python': ['cachetools'], }, 'data': [], - 'installable': False, + 'installable': True, } From 354f89b21f87084f29e4487a36656d539a073cd7 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Oct 2017 10:43:50 +0200 Subject: [PATCH 23/80] PY3: apply automated changes by 2to3 on addons --- component_event/components/event.py | 2 +- component_event/models/base.py | 5 +- component_event/tests/test_event.py | 86 ++++++++++++++--------------- 3 files changed, 47 insertions(+), 46 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index ac6ab1533..54806934f 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -247,7 +247,7 @@ def _collect_events(self, name): def _init_collected_events(self, class_events): events = set() - for cls, names in class_events.iteritems(): + for cls, names in class_events.items(): for name in names: component = cls(self.work) events.add(getattr(component, name)) diff --git a/component_event/models/base.py b/component_event/models/base.py index d2ca468a5..e4879f6b4 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -91,13 +91,14 @@ def button_do_something(self): @api.model def create(self, vals): record = super(Base, self).create(vals) - self._event('on_record_create').notify(record, fields=vals.keys()) + fields = list(vals.keys()) + self._event('on_record_create').notify(record, fields=fields) return record @api.multi def write(self, vals): result = super(Base, self).write(vals) - fields = vals.keys() + fields = list(vals.keys()) for record in self: self._event('on_record_write').notify(record, fields=fields) return result diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index 77370ee3b..e92e532f9 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import mock -import unittest2 +import unittest from odoo.addons.component.tests.common import ( ComponentRegistryCase, @@ -14,7 +14,7 @@ from odoo.addons.component_event.components.event import skip_if -class TestEventWorkContext(unittest2.TestCase): +class TestEventWorkContext(unittest.TestCase): """ Test Events Components """ def setUp(self): @@ -27,8 +27,8 @@ def test_env(self): """ WorkContext with env """ work = EventWorkContext(model_name='res.users', env=self.env, components_registry=self.components_registry) - self.assertEquals(self.env, work.env) - self.assertEquals('res.users', work.model_name) + self.assertEqual(self.env, work.env) + self.assertEqual('res.users', work.model_name) with self.assertRaises(ValueError): # pylint: disable=W0104 work.collection # noqa @@ -40,9 +40,9 @@ def test_collection(self): collection.env = env work = EventWorkContext(model_name='res.users', collection=collection, components_registry=self.components_registry) - self.assertEquals(collection, work.collection) - self.assertEquals(env, work.env) - self.assertEquals('res.users', work.model_name) + self.assertEqual(collection, work.collection) + self.assertEqual(env, work.env) + self.assertEqual('res.users', work.model_name) def test_env_and_collection(self): """ WorkContext with collection and env is forbidden """ @@ -68,10 +68,10 @@ def test_env_work_on(self): work = EventWorkContext(env=env, model_name='res.users', components_registry=self.components_registry) work2 = work.work_on(model_name='res.partner', collection=collection) - self.assertEquals('WorkContext', work2.__class__.__name__) - self.assertEquals(env, work2.env) - self.assertEquals('res.partner', work2.model_name) - self.assertEquals(self.components_registry, work2.components_registry) + self.assertEqual('WorkContext', work2.__class__.__name__) + self.assertEqual(env, work2.env) + self.assertEqual('res.partner', work2.model_name) + self.assertEqual(self.components_registry, work2.components_registry) with self.assertRaises(ValueError): # pylint: disable=W0104 work.collection # noqa @@ -84,11 +84,11 @@ def test_collection_work_on(self): work = EventWorkContext(collection=collection, model_name='res.users', components_registry=self.components_registry) work2 = work.work_on(model_name='res.partner') - self.assertEquals('WorkContext', work2.__class__.__name__) - self.assertEquals(collection, work2.collection) - self.assertEquals(env, work2.env) - self.assertEquals('res.partner', work2.model_name) - self.assertEquals(self.components_registry, work2.components_registry) + self.assertEqual('WorkContext', work2.__class__.__name__) + self.assertEqual(collection, work2.collection) + self.assertEqual(env, work2.env) + self.assertEqual('res.partner', work2.model_name) + self.assertEqual(self.components_registry, work2.components_registry) def test_collection_work_on_collection(self): """ WorkContext collection changed with work_on """ @@ -101,11 +101,11 @@ def test_collection_work_on_collection(self): # when work_on is used inside an event component, we want # to switch back to a normal WorkContext, because we don't # need anymore the EventWorkContext - self.assertEquals('WorkContext', work2.__class__.__name__) - self.assertEquals(collection, work2.collection) - self.assertEquals(env, work2.env) - self.assertEquals('res.users', work2.model_name) - self.assertEquals(self.components_registry, work2.components_registry) + self.assertEqual('WorkContext', work2.__class__.__name__) + self.assertEqual(collection, work2.collection) + self.assertEqual(env, work2.env) + self.assertEqual('res.users', work2.model_name) + self.assertEqual(self.components_registry, work2.components_registry) class TestEvent(ComponentRegistryCase): @@ -144,7 +144,7 @@ def on_record_create(self, recipient, something, fields=None): self.collecter.collect_events('on_record_create').notify( recipient, something, fields=fields ) - self.assertEquals([('OK', something, fields)], recipient) + self.assertEqual([('OK', something, fields)], recipient) def test_collect_several(self): class MyEventListener(Component): @@ -173,11 +173,11 @@ def on_record_create(self, recipient, something, fields=None): # collect the event and notify them collected = self.collecter.collect_events('on_record_create') - self.assertEquals(2, len(collected.events)) + self.assertEqual(2, len(collected.events)) collected.notify(recipient, something, fields=fields) - self.assertEquals([('OK', something, fields), - ('OK', something, fields)], recipient) + self.assertEqual([('OK', something, fields), + ('OK', something, fields)], recipient) def test_event_cache(self): class MyEventListener(Component): @@ -192,10 +192,10 @@ def on_record_create(self): # collect the event collected = self.collecter.collect_events('on_record_create') # CollectedEvents.events contains the collected events - self.assertEquals(1, len(collected.events)) + self.assertEqual(1, len(collected.events)) event = list(collected.events)[0] - self.assertEquals(self.work, event.im_self.work) - self.assertEquals(self.work.env, event.im_self.work.env) + self.assertEqual(self.work, event.__self__.work) + self.assertEqual(self.work.env, event.__self__.work.env) # build and register a new listener class MyOtherEventListener(Component): @@ -215,21 +215,21 @@ def on_record_create(self): collecter = self.comp_registry['base.event.collecter'](work) collected = collecter.collect_events('on_record_create') # CollectedEvents.events contains the collected events - self.assertEquals(1, len(collected.events)) + self.assertEqual(1, len(collected.events)) event = list(collected.events)[0] - self.assertEquals(work, event.im_self.work) - self.assertEquals(env, event.im_self.work.env) + self.assertEqual(work, event.__self__.work) + self.assertEqual(env, event.__self__.work.env) # if we empty the cache, as it on the class, both collecters # should now find the 2 events collecter._cache.clear() self.comp_registry._cache.clear() # CollectedEvents.events contains the collected events - self.assertEquals( + self.assertEqual( 2, len(collecter.collect_events('on_record_create').events) ) - self.assertEquals( + self.assertEqual( 2, len(self.collecter.collect_events('on_record_create').events) ) @@ -247,7 +247,7 @@ def on_record_create(self): # collect the event collected = self.collecter.collect_events('on_record_create') # CollectedEvents.events contains the collected events - self.assertEquals(1, len(collected.events)) + self.assertEqual(1, len(collected.events)) # build and register a new listener class MyOtherEventListener(Component): @@ -271,7 +271,7 @@ def on_record_create(self): collected = collecter.collect_events('on_record_create') # for a different collection, we should not have the same # cache entry - self.assertEquals(2, len(collected.events)) + self.assertEqual(2, len(collected.events)) def test_event_cache_model_name(self): class MyEventListener(Component): @@ -286,7 +286,7 @@ def on_record_create(self): # collect the event collected = self.collecter.collect_events('on_record_create') # CollectedEvents.events contains the collected events - self.assertEquals(1, len(collected.events)) + self.assertEqual(1, len(collected.events)) # build and register a new listener class MyOtherEventListener(Component): @@ -308,7 +308,7 @@ def on_record_create(self): collected = collecter.collect_events('on_record_create') # for a different collection, we should not have the same # cache entry - self.assertEquals(2, len(collected.events)) + self.assertEqual(2, len(collected.events)) def test_skip_if(self): class MyEventListener(Component): @@ -330,7 +330,7 @@ def on_record_create(self, msg): # collect the event and notify it collected = self.collecter.collect_events('on_record_create') - self.assertEquals(2, len(collected.events)) + self.assertEqual(2, len(collected.events)) collected.notify('foo') @@ -360,7 +360,7 @@ def on_foo(self, record, name): events = partner._event('on_foo', components_registry=self.comp_registry) events.notify(partner, 'bar') - self.assertEquals('bar', partner.name) + self.assertEqual('bar', partner.name) def test_event_filter_on_model(self): class GlobalListener(Component): @@ -393,8 +393,8 @@ def on_foo(self, record, name): 'on_foo', components_registry=self.comp_registry ).notify(partner, 'bar') - self.assertEquals('bar', partner.name) - self.assertEquals('bar', partner.ref) + self.assertEqual('bar', partner.name) + self.assertEqual('bar', partner.ref) def test_event_filter_on_collection(self): class GlobalListener(Component): @@ -428,5 +428,5 @@ def on_foo(self, record, name): components_registry=self.comp_registry ) events.notify(partner, 'bar') - self.assertEquals('bar', partner.name) - self.assertEquals('bar', partner.ref) + self.assertEqual('bar', partner.name) + self.assertEqual('bar', partner.ref) From d41732c35fd7cb427347a75d607c9439c717d8ec Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Oct 2017 16:58:21 +0200 Subject: [PATCH 24/80] Fix some pylint-odoo warnings --- component_event/README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/component_event/README.rst b/component_event/README.rst index 56ecde7c0..d3d986b20 100644 --- a/component_event/README.rst +++ b/component_event/README.rst @@ -66,8 +66,6 @@ This module triggers 3 events: Known issues / Roadmap ====================== -* ... - Bug Tracker =========== From 2c2d9830c9e75aafd350f1323dfcd39cb5c9a05d Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Sat, 17 Feb 2018 03:25:32 +0100 Subject: [PATCH 25/80] OCA Transbot updated translations from Transifex --- component_event/i18n/fr.po | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 component_event/i18n/fr.po diff --git a/component_event/i18n/fr.po b/component_event/i18n/fr.po new file mode 100644 index 000000000..6f501e192 --- /dev/null +++ b/component_event/i18n/fr.po @@ -0,0 +1,24 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component_event +# +# Translators: +# Nicolas JEUDY , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-02-01 01:48+0000\n" +"PO-Revision-Date: 2018-02-01 01:48+0000\n" +"Last-Translator: Nicolas JEUDY , 2018\n" +"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: component_event +#: model:ir.model,name:component_event.model_base +msgid "base" +msgstr "base" From b3e0175397055d57a47eed52fe8e2de9cff8827f Mon Sep 17 00:00:00 2001 From: oca-travis Date: Sat, 23 Jun 2018 00:39:51 +0000 Subject: [PATCH 26/80] [UPD] Update component_event.pot --- component_event/i18n/component_event.pot | 20 ++++++++++++++++++++ component_event/i18n/fr.po | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 component_event/i18n/component_event.pot diff --git a/component_event/i18n/component_event.pot b/component_event/i18n/component_event.pot new file mode 100644 index 000000000..74990c4d8 --- /dev/null +++ b/component_event/i18n/component_event.pot @@ -0,0 +1,20 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component_event +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: component_event +#: model:ir.model,name:component_event.model_base +msgid "base" +msgstr "" + diff --git a/component_event/i18n/fr.po b/component_event/i18n/fr.po index 6f501e192..fe6092fc8 100644 --- a/component_event/i18n/fr.po +++ b/component_event/i18n/fr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component_event -# +# # Translators: # Nicolas JEUDY , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-02-01 01:48+0000\n" "Last-Translator: Nicolas JEUDY , 2018\n" "Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: component_event From 66254a7f642f08700040ff0cd31d40256f588467 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 30 Mar 2018 09:22:09 +0200 Subject: [PATCH 27/80] Improve documentation of APIs * Sphinx crashes with a recursion error when trying to document Models fields. Remove the fields from the documentation altogether, they didn't bring much value anyway * Gone through all the API pages and removed many useless autodoc with a mix of __all__, :exclude-members: or by defining more precisely what we want with :autoclass:, :autoatttribute:, ... or simply removing them * Changed the order of a few autodocs by putting the most useful classes/functions first * Improved a few docstrings --- component_event/components/event.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/component_event/components/event.py b/component_event/components/event.py index 54806934f..40fea0d2f 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -122,6 +122,8 @@ def on_record_create(self, record, fields=None): except ImportError: _logger.debug("Cannot import 'cachetools'.") +__all__ = ['skip_if'] + # 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 From 0cf359ede7f629d06bead075207ba535b753704f Mon Sep 17 00:00:00 2001 From: OCA git bot Date: Thu, 27 Sep 2018 01:59:42 +0200 Subject: [PATCH 28/80] [MIG] Make modules uninstallable --- component_event/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component_event/__manifest__.py b/component_event/__manifest__.py index 86c3a1d91..eba2bfd08 100644 --- a/component_event/__manifest__.py +++ b/component_event/__manifest__.py @@ -15,5 +15,5 @@ 'python': ['cachetools'], }, 'data': [], - 'installable': True, + 'installable': False, } From 9a1dc8776044ac1540c0d6bdcf7d64b5868f1ac8 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 26 Nov 2018 09:14:48 +0100 Subject: [PATCH 29/80] Migrate component_event to 12.0 --- component_event/README.rst | 90 ++++++++++++++++--------- component_event/__manifest__.py | 6 +- component_event/models/base.py | 2 + component_event/readme/CONTRIBUTORS.rst | 1 + component_event/readme/DESCRIPTION.rst | 7 ++ component_event/readme/HISTORY.rst | 17 +++++ component_event/readme/USAGE.rst | 34 ++++++++++ 7 files changed, 123 insertions(+), 34 deletions(-) create mode 100644 component_event/readme/CONTRIBUTORS.rst create mode 100644 component_event/readme/DESCRIPTION.rst create mode 100644 component_event/readme/HISTORY.rst create mode 100644 component_event/readme/USAGE.rst diff --git a/component_event/README.rst b/component_event/README.rst index d3d986b20..cab488018 100644 --- a/component_event/README.rst +++ b/component_event/README.rst @@ -1,10 +1,26 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl - :alt: License: AGPL-3 - -=================== -Components - Events -=================== +================= +Components Events +================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github + :target: https://github.com/OCA/connector/tree/12.0/component_event + :alt: OCA/connector +.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/102/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| This module implements an event system (`Observer pattern`_) and is a base block for the Connector Framework. It can be used without @@ -14,15 +30,10 @@ Documentation: http://odoo-connector.com/ .. _Observer pattern: https://en.wikipedia.org/wiki/Observer_pattern -Installation -============ - -* Install ``component_event`` - -Configuration -============= +**Table of contents** -The module does nothing by itself and has no configuration. +.. contents:: + :local: Usage ===== @@ -62,45 +73,62 @@ This module triggers 3 events: * ``on_record_write(record, fields=None)`` * ``on_record_unlink(record)`` +Changelog +========= + +.. [ The change log. The goal of this file is to help readers + understand changes between version. The primary audience is + end users and integrators. Purely technical changes such as + code refactoring must not be mentioned here. + + This file may contain ONE level of section titles, underlined + with the ~ (tilde) character. Other section markers are + forbidden and will likely break the structure of the README.rst + or other documents where this fragment is included. ] + +Next +~~~~ + +12.0.1.0.0 (2018-11-26) +~~~~~~~~~~~~~~~~~~~~~~~ -Known issues / Roadmap -====================== +* [MIGRATION] from 12.0 branched at rev. 324e006 Bug Tracker =========== -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smash it by providing detailed and welcomed feedback. +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed feedback. + +Do not contact contributors directly about support or help with technical issues. Credits ======= -Images ------- +Authors +~~~~~~~ -* Odoo Community Association: `Icon `_. +* Camptocamp Contributors ------------- +~~~~~~~~~~~~ * Guewen Baconnier -Do not contact contributors directly about support or help with technical issues. +Maintainers +~~~~~~~~~~~ -Maintainer ----------- +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://odoo-community.org -This module is maintained by the OCA. - OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. -To contribute to this module, please visit https://odoo-community.org. +This module is part of the `OCA/connector `_ project on GitHub. +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/component_event/__manifest__.py b/component_event/__manifest__.py index eba2bfd08..7eba6d959 100644 --- a/component_event/__manifest__.py +++ b/component_event/__manifest__.py @@ -3,10 +3,10 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) {'name': 'Components Events', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', 'author': 'Camptocamp,' 'Odoo Community Association (OCA)', - 'website': 'https://www.camptocamp.com', + 'website': 'http://odoo-connector.com', 'license': 'AGPL-3', 'category': 'Generic Modules', 'depends': ['component', @@ -15,5 +15,5 @@ 'python': ['cachetools'], }, 'data': [], - 'installable': False, + 'installable': True, } diff --git a/component_event/models/base.py b/component_event/models/base.py index e4879f6b4..f1386ba54 100644 --- a/component_event/models/base.py +++ b/component_event/models/base.py @@ -75,6 +75,8 @@ def button_do_something(self): # during the initialization. Hence we return an empty list of # events, the 'notify' calls will do nothing. return CollectedEvents([]) + if not comp_registry.get('base.event.collecter'): + return CollectedEvents([]) model_name = self._name if collection is not None: diff --git a/component_event/readme/CONTRIBUTORS.rst b/component_event/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..48286263c --- /dev/null +++ b/component_event/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Guewen Baconnier diff --git a/component_event/readme/DESCRIPTION.rst b/component_event/readme/DESCRIPTION.rst new file mode 100644 index 000000000..6216dded1 --- /dev/null +++ b/component_event/readme/DESCRIPTION.rst @@ -0,0 +1,7 @@ +This module implements an event system (`Observer pattern`_) and is a +base block for the Connector Framework. It can be used without +using the full Connector though. It is built upon the ``component`` module. + +Documentation: http://odoo-connector.com/ + +.. _Observer pattern: https://en.wikipedia.org/wiki/Observer_pattern diff --git a/component_event/readme/HISTORY.rst b/component_event/readme/HISTORY.rst new file mode 100644 index 000000000..95c16866e --- /dev/null +++ b/component_event/readme/HISTORY.rst @@ -0,0 +1,17 @@ +.. [ The change log. The goal of this file is to help readers + understand changes between version. The primary audience is + end users and integrators. Purely technical changes such as + code refactoring must not be mentioned here. + + This file may contain ONE level of section titles, underlined + with the ~ (tilde) character. Other section markers are + forbidden and will likely break the structure of the README.rst + or other documents where this fragment is included. ] + +Next +~~~~ + +12.0.1.0.0 (2018-11-26) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [MIGRATION] from 12.0 branched at rev. 324e006 diff --git a/component_event/readme/USAGE.rst b/component_event/readme/USAGE.rst new file mode 100644 index 000000000..327bd141f --- /dev/null +++ b/component_event/readme/USAGE.rst @@ -0,0 +1,34 @@ +As a developer, you have access to a events system. You can find the +documentation in the code or on http://odoo-connector.com + +In a nutshell, you can create trigger events:: + + 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 + +And subscribe listeners to the events:: + + from odoo.addons.component.core import Component + from odoo.addons.component_event import skip_if + + class MagentoListener(Component): + _name = 'magento.event.listener' + _inherit = 'base.connector.listener' + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_create(self, record, fields=None): + """ Called when a record is created """ + record.with_delay().export_record(fields=fields) + + +This module triggers 3 events: + +* ``on_record_create(record, fields=None)`` +* ``on_record_write(record, fields=None)`` +* ``on_record_unlink(record)`` From 2d8f27b4d850fc3594eaaa9c17c2312f38831b31 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 14 Jan 2019 11:01:24 +0000 Subject: [PATCH 30/80] [UPD] README.rst --- component_event/README.rst | 10 +- component_event/static/description/index.html | 485 ++++++++++++++++++ 2 files changed, 492 insertions(+), 3 deletions(-) create mode 100644 component_event/static/description/index.html diff --git a/component_event/README.rst b/component_event/README.rst index cab488018..15b739dda 100644 --- a/component_event/README.rst +++ b/component_event/README.rst @@ -16,11 +16,14 @@ Components Events .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github :target: https://github.com/OCA/connector/tree/12.0/component_event :alt: OCA/connector -.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/connector-12-0/connector-12-0-component_event + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png :target: https://runbot.odoo-community.org/runbot/102/12.0 :alt: Try me on Runbot -|badge1| |badge2| |badge3| |badge4| +|badge1| |badge2| |badge3| |badge4| |badge5| This module implements an event system (`Observer pattern`_) and is a base block for the Connector Framework. It can be used without @@ -99,7 +102,8 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us smashing it by providing a detailed and welcomed feedback. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. Do not contact contributors directly about support or help with technical issues. diff --git a/component_event/static/description/index.html b/component_event/static/description/index.html new file mode 100644 index 000000000..0f4f78acb --- /dev/null +++ b/component_event/static/description/index.html @@ -0,0 +1,485 @@ + + + + + + +Components Events + + + +
+

Components Events

+ + +

Beta License: AGPL-3 OCA/connector Translate me on Weblate Try me on Runbot

+

This module implements an event system (Observer pattern) and is a +base block for the Connector Framework. It can be used without +using the full Connector though. It is built upon the component module.

+

Documentation: http://odoo-connector.com/

+

Table of contents

+ +
+

Usage

+

As a developer, you have access to a events system. You can find the +documentation in the code or on http://odoo-connector.com

+

In a nutshell, you can create trigger events:

+
+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
+
+

And subscribe listeners to the events:

+
+from odoo.addons.component.core import Component
+from odoo.addons.component_event import skip_if
+
+class MagentoListener(Component):
+    _name = 'magento.event.listener'
+    _inherit = 'base.connector.listener'
+
+    @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
+    def on_record_create(self, record, fields=None):
+        """ Called when a record is created """
+        record.with_delay().export_record(fields=fields)
+
+

This module triggers 3 events:

+
    +
  • on_record_create(record, fields=None)
  • +
  • on_record_write(record, fields=None)
  • +
  • on_record_unlink(record)
  • +
+
+
+

Changelog

+ + +
+

12.0.1.0.0 (2018-11-26)

+
    +
  • [MIGRATION] from 12.0 branched at rev. 324e006
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/connector project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From e43c8c6f0200416656efa4242deb19123df81051 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Mon, 14 Jan 2019 11:27:32 +0000 Subject: [PATCH 31/80] [UPD] Update component_event.pot --- component_event/i18n/component_event.pot | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component_event/i18n/component_event.pot b/component_event/i18n/component_event.pot index 74990c4d8..a1a87c6ab 100644 --- a/component_event/i18n/component_event.pot +++ b/component_event/i18n/component_event.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 11.0\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: <>\n" "Language-Team: \n" @@ -15,6 +15,6 @@ msgstr "" #. module: component_event #: model:ir.model,name:component_event.model_base -msgid "base" +msgid "Base" msgstr "" From 3e2640b826817298d7cd655563dbe3a20c673924 Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Sun, 27 Jan 2019 10:35:10 +0000 Subject: [PATCH 32/80] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: connector-12.0/connector-12.0-component_event Translate-URL: https://translation.odoo-community.org/projects/connector-12-0/connector-12-0-component_event/ --- component_event/i18n/fr.po | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/component_event/i18n/fr.po b/component_event/i18n/fr.po index fe6092fc8..f5c6daaae 100644 --- a/component_event/i18n/fr.po +++ b/component_event/i18n/fr.po @@ -20,5 +20,8 @@ msgstr "" #. module: component_event #: model:ir.model,name:component_event.model_base -msgid "base" -msgstr "base" +msgid "Base" +msgstr "" + +#~ msgid "base" +#~ msgstr "base" From 0aa1c023ce729e7b841052391a1f8fcdfea68c7a Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Tue, 19 Mar 2019 21:55:27 +0200 Subject: [PATCH 33/80] component, component_event: tag unittest.TestCase subclasses Since v12.0, direct subclasses of `unittest.TestCase` which do not have :class:`odoo.tests.common.MetaCase` as a meta-class must be explicitly tagged with :method:`odoo.tests.common.tagged`, otherwise they will not be picked up by the test runner. E.g.: ```python @tagged('standard', 'at_install') ``` --- component_event/tests/test_event.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/component_event/tests/test_event.py b/component_event/tests/test_event.py index e92e532f9..450eed7e5 100644 --- a/component_event/tests/test_event.py +++ b/component_event/tests/test_event.py @@ -12,8 +12,10 @@ from odoo.addons.component.core import Component from odoo.addons.component_event.core import EventWorkContext from odoo.addons.component_event.components.event import skip_if +from odoo.tests.common import tagged +@tagged('standard', 'at_install') class TestEventWorkContext(unittest.TestCase): """ Test Events Components """ From c87ef3747c6caebf17394a687e8e97ebb5b0b0b1 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 3 Apr 2019 02:39:17 +0000 Subject: [PATCH 34/80] [ADD] icon.png --- component_event/static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 component_event/static/description/icon.png diff --git a/component_event/static/description/icon.png b/component_event/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From 0cbb125211a4fc3901da5673d11654013a626f40 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 12 Apr 2019 14:48:12 +0200 Subject: [PATCH 35/80] Fix compatibility with cachetools 3.0.0 It doesn't allow anymore to refer to 'self' in the key. Add an indirection level using another method that takes the cached arguments. See discussion on https://github.com/tkem/cachetools/issues/107 --- component_event/components/event.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/component_event/components/event.py b/component_event/components/event.py index 40fea0d2f..8288fa1b1 100644 --- a/component_event/components/event.py +++ b/component_event/components/event.py @@ -118,7 +118,7 @@ def on_record_create(self, record, fields=None): _logger = logging.getLogger(__name__) try: - from cachetools import LRUCache, cachedmethod, keys + from cachetools import LRUCache, cachedmethod except ImportError: _logger.debug("Cannot import 'cachetools'.") @@ -225,22 +225,23 @@ def _complete_component_build(cls): # 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): + collection_name = None + if self.work._collection is not None: + collection_name = self.work.collection._name + return self._collect_events_cached( + collection_name, + self.work.model_name, + name + ) + + @cachedmethod(operator.attrgetter('_cache')) + def _collect_events_cached(self, collection_name, model_name, 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, + model_name=model_name, ) for cls in component_classes: if cls.has_event(name): From 5d85f6b897156d71c7c166675408c1ad1104bfdc Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 29 Jul 2019 02:44:06 +0000 Subject: [PATCH 36/80] [UPD] README.rst --- component_event/static/description/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component_event/static/description/index.html b/component_event/static/description/index.html index 0f4f78acb..72e6768b6 100644 --- a/component_event/static/description/index.html +++ b/component_event/static/description/index.html @@ -3,7 +3,7 @@ - + Components Events