From 46b0d7741865807755b857a7fe69471c78318a5e Mon Sep 17 00:00:00 2001 From: Oliver Lippert Date: Sun, 1 Oct 2023 12:10:26 +0200 Subject: [PATCH] Let plugins decide if events should be processed or not (#5618) * add wants_process_event to EventMixin in register_event this function get called and the task is just offload if the plugin returns true * add tests for wants_process_event * fix comments for register_event after code changes * add documentation for EventMixin.wants_process_event * avoid pre-formatting log messages --------- Co-authored-by: Oliver Lippert --- InvenTree/plugin/base/event/events.py | 37 +++++++------ InvenTree/plugin/base/event/mixins.py | 9 ++++ .../samples/event/filtered_event_sample.py | 32 ++++++++++++ .../event/test_filtered_event_sample.py | 52 +++++++++++++++++++ docs/docs/extend/plugins/event.md | 32 +++++++++++- 5 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 InvenTree/plugin/samples/event/filtered_event_sample.py create mode 100644 InvenTree/plugin/samples/event/test_filtered_event_sample.py diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py index 24eff73153..ef3fa69a91 100644 --- a/InvenTree/plugin/base/event/events.py +++ b/InvenTree/plugin/base/event/events.py @@ -65,27 +65,32 @@ def register_event(event, *args, **kwargs): with transaction.atomic(): for slug, plugin in registry.plugins.items(): + if not plugin.mixin_enabled('events'): + continue - if plugin.mixin_enabled('events'): + # Only allow event registering for 'active' plugins + if not plugin.is_active(): + continue - if plugin.is_active(): - # Only allow event registering for 'active' plugins + # Let the plugin decide if it wants to process this event + if not plugin.wants_process_event(event): + continue - logger.debug("Registering callback for plugin '%s'", slug) + logger.debug("Registering callback for plugin '%s'", slug) - # This task *must* be processed by the background worker, - # unless we are running CI tests - if 'force_async' not in kwargs and not settings.PLUGIN_TESTING_EVENTS: - kwargs['force_async'] = True + # This task *must* be processed by the background worker, + # unless we are running CI tests + if 'force_async' not in kwargs and not settings.PLUGIN_TESTING_EVENTS: + kwargs['force_async'] = True - # Offload a separate task for each plugin - offload_task( - process_event, - slug, - event, - *args, - **kwargs - ) + # Offload a separate task for each plugin + offload_task( + process_event, + slug, + event, + *args, + **kwargs + ) def process_event(plugin_slug, event, *args, **kwargs): diff --git a/InvenTree/plugin/base/event/mixins.py b/InvenTree/plugin/base/event/mixins.py index 2aa67b87a9..9030eb2d22 100644 --- a/InvenTree/plugin/base/event/mixins.py +++ b/InvenTree/plugin/base/event/mixins.py @@ -9,6 +9,15 @@ class EventMixin: Implementing classes must provide a "process_event" function: """ + def wants_process_event(self, event): + """Function to subscribe to events. + + Return true if you're interested in the given event, false if not. + """ + + # Default implementation always returns true (backwards compatibility) + return True + def process_event(self, event, *args, **kwargs): """Function to handle events. diff --git a/InvenTree/plugin/samples/event/filtered_event_sample.py b/InvenTree/plugin/samples/event/filtered_event_sample.py new file mode 100644 index 0000000000..4267f3be1b --- /dev/null +++ b/InvenTree/plugin/samples/event/filtered_event_sample.py @@ -0,0 +1,32 @@ +"""Sample plugin which responds to events.""" + +import logging + +from django.conf import settings + +from plugin import InvenTreePlugin +from plugin.mixins import EventMixin + +logger = logging.getLogger('inventree') + + +class FilteredEventPluginSample(EventMixin, InvenTreePlugin): + """A sample plugin which provides supports for triggered events.""" + + NAME = "FilteredEventPlugin" + SLUG = "filteredsampleevent" + TITLE = "Triggered by test.event only" + + def wants_process_event(self, event): + """Return whether given event should be processed or not.""" + return event == "test.event" + + def process_event(self, event, *args, **kwargs): + """Custom event processing.""" + print(f"Processing triggered event: '{event}'") + print("args:", str(args)) + print("kwargs:", str(kwargs)) + + # Issue warning that we can test for + if settings.PLUGIN_TESTING: + logger.debug('Event `%s` triggered in sample plugin', event) diff --git a/InvenTree/plugin/samples/event/test_filtered_event_sample.py b/InvenTree/plugin/samples/event/test_filtered_event_sample.py new file mode 100644 index 0000000000..efcb5c0290 --- /dev/null +++ b/InvenTree/plugin/samples/event/test_filtered_event_sample.py @@ -0,0 +1,52 @@ +"""Unit tests for event_sample sample plugins.""" + +from django.conf import settings +from django.test import TestCase + +from common.models import InvenTreeSetting +from plugin import registry +from plugin.base.event.events import trigger_event + +from .filtered_event_sample import logger + + +class FilteredEventPluginSampleTests(TestCase): + """Tests for EventPluginSample.""" + + def test_run_event(self): + """Check if the event is issued.""" + # Activate plugin + config = registry.get_plugin('filteredsampleevent').plugin_config() + config.active = True + config.save() + + InvenTreeSetting.set_setting('ENABLE_PLUGINS_EVENTS', True, change_user=None) + + # Enable event testing + settings.PLUGIN_TESTING_EVENTS = True + # Check that an event is issued + with self.assertLogs(logger=logger, level="DEBUG") as cm: + trigger_event('test.event') + self.assertIn('DEBUG:inventree:Event `test.event` triggered in sample plugin', cm[1]) + + # Disable again + settings.PLUGIN_TESTING_EVENTS = False + + def test_ignore_event(self): + """Check if the event is issued.""" + # Activate plugin + config = registry.get_plugin('filteredsampleevent').plugin_config() + config.active = True + config.save() + + InvenTreeSetting.set_setting('ENABLE_PLUGINS_EVENTS', True, change_user=None) + + # Enable event testing + settings.PLUGIN_TESTING_EVENTS = True + # Check that an event is issued + with self.assertLogs(logger=logger, level="DEBUG") as cm: + trigger_event('test.some.other.event') + self.assertNotIn('DEBUG:inventree:Event `test.some.other.event` triggered in sample plugin', cm[1]) + + # Disable again + settings.PLUGIN_TESTING_EVENTS = False diff --git a/docs/docs/extend/plugins/event.md b/docs/docs/extend/plugins/event.md index 44742fbeb3..4fba5aa956 100644 --- a/docs/docs/extend/plugins/event.md +++ b/docs/docs/extend/plugins/event.md @@ -15,9 +15,9 @@ When a certain (server-side) event occurs, the background worker passes the even {% include 'img.html' %} {% endwith %} -### Example +### Example (all events) -Implementing classes must provide a `process_event` function: +Implementing classes must at least provide a `process_event` function: ```python class EventPlugin(EventMixin, InvenTreePlugin): @@ -36,6 +36,34 @@ class EventPlugin(EventMixin, InvenTreePlugin): print(f"Processing triggered event: '{event}'") ``` +### Example (specific events) + +If you want to process just some specific events, you can also implement the `wants_process_event` function to decide if you want to process this event or not. This function will be executed synchronously, so be aware that it should contain simple logic. + +Overall this function can reduce the workload on the background workers significantly since less events are queued to be processed. + +```python +class EventPlugin(EventMixin, InvenTreePlugin): + """ + A simple example plugin which responds to 'salesordershipment.completed' event on the InvenTree server. + + This example simply prints out the event information. + A more complex plugin can run enhanced logic on this event. + """ + + NAME = "EventPlugin" + SLUG = "event" + TITLE = "Triggered Events" + + def wants_process_event(self, event): + """Here you can decide if this event should be send to `process_event` or not.""" + return event == "salesordershipment.completed" + + def process_event(self, event, *args, **kwargs): + """Here you can run you'r specific logic.""" + print(f"Sales order was completely shipped: '{args}' '{kwargs}'") +``` + ### Events Events are passed through using a string identifier, e.g. `build.completed`