From 04d25a60b0653304ee541192920a2c737a73ac7d Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 8 Jan 2022 09:07:27 +1100 Subject: [PATCH] Adds sample plugin which responds to triggered events - Adds some example trigger events for the "Part" model --- InvenTree/part/models.py | 12 ++++-- .../plugin/builtin/integration/mixins.py | 4 +- InvenTree/plugin/events.py | 25 +++++++++++- InvenTree/plugin/plugin.py | 12 ++++++ InvenTree/plugin/registry.py | 39 +++++++++++++++++++ .../samples/integration/event_sample.py | 29 ++++++++++++++ .../samples/integration/scheduled_task.py | 2 +- .../InvenTree/settings/plugin_settings.html | 2 +- 8 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 InvenTree/plugin/samples/integration/event_sample.py diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index ec76846a08..99d7cd02f5 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -60,6 +60,8 @@ import common.models import part.settings as part_settings +from plugin.events import trigger_event + logger = logging.getLogger("inventree") @@ -1980,10 +1982,10 @@ class Part(MPTTModel): @property def attachment_count(self): - """ Count the number of attachments for this part. + """ + Count the number of attachments for this part. If the part is a variant of a template part, include the number of attachments for the template part. - """ return self.part_attachments.count() @@ -2181,7 +2183,11 @@ def after_save_part(sender, instance: Part, created, **kwargs): Function to be executed after a Part is saved """ - if not created: + trigger_event('part.saved', part_id=instance.pk) + + if created: + trigger_event('part.created', part_id=instance.pk) + else: # Check part stock only if we are *updating* the part (not creating it) # Run this check in the background diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 68288121e2..dff749af53 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -189,10 +189,10 @@ class EventMixin: which provide pairs of 'event':'function' Notes: - + Events are called by name, and based on the django signal nomenclature, e.g. 'part.pre_save' - + Receiving functions must be prototyped to match the 'event' they receive. Example: diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index 703fe71d9c..bd3bf7c06c 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -8,11 +8,14 @@ from __future__ import unicode_literals import logging from django.conf import settings +from django.db import transaction from common.models import InvenTreeSetting from InvenTree.tasks import offload_task +from plugin.registry import plugin_registry + logger = logging.getLogger('inventree') @@ -38,12 +41,30 @@ def trigger_event(event, *args, **kwargs): def process_event(event, *args, **kwargs): """ Respond to a triggered event. - + This function is run by the background worker process. + + This function may queue multiple functions to be handled by the background worker. """ logger.info(f"Processing event '{event}'") # Determine if there are any plugins which are interested in responding if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'): - pass + + # Run atomically, to ensure that either *all* tasks are registered, or *none* + with transaction.atomic(): + for slug, callbacks in plugin_registry.mixins_events.items(): + # slug = plugin slug + # callbacks = list of (event, function) tuples + + for _event, _func in callbacks: + if _event == event: + + logger.info(f"Running task '{_func}' for plugin '{slug}'") + + offload_task( + _func, + *args, + **kwargs + ) diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py index 35643b36c3..8842553ba7 100644 --- a/InvenTree/plugin/plugin.py +++ b/InvenTree/plugin/plugin.py @@ -63,3 +63,15 @@ class InvenTreePlugin(): raise error return cfg + + def is_active(self): + """ + Return True if this plugin is currently active + """ + + cfg = self.plugin_config() + + if cfg: + return cfg.active + else: + return False diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index b7e37d22ba..5e6016fc86 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -56,8 +56,10 @@ class PluginsRegistry: # integration specific self.installed_apps = [] # Holds all added plugin_paths + # mixins self.mixins_settings = {} + self.mixins_events = {} # region public plugin functions def load_plugins(self): @@ -265,6 +267,7 @@ class PluginsRegistry: logger.info(f'Found {len(plugins)} active plugins') self.activate_integration_settings(plugins) + self.activate_integration_events(plugins) self.activate_integration_schedule(plugins) self.activate_integration_app(plugins, force_reload=force_reload) @@ -275,6 +278,7 @@ class PluginsRegistry: self.deactivate_integration_app() self.deactivate_integration_schedule() + self.deactivate_integration_events() self.deactivate_integration_settings() def activate_integration_settings(self, plugins): @@ -299,6 +303,41 @@ class PluginsRegistry: # clear cache self.mixins_settings = {} + def activate_integration_events(self, plugins): + """ + Activate triggered events mixin for applicable plugins + """ + + logger.info('Activating plugin events') + + from common.models import InvenTreeSetting + + self.mixins_events = {} + + event_count = 0 + + if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'): + + for slug, plugin in plugins: + + if plugin.mixin_enabled('events'): + config = plugin.plugin_config() + + # Only activate events for plugins which are enabled + if config and config.active: + self.mixins_events[slug] = plugin.events + + event_count += len(plugin.events) + + if event_count > 0: + logger.info(f"Registered callbacks for {event_count} events") + + def deactivate_integration_events(self): + """ + Deactivate events for all plugins + """ + self.mixins_events = {} + def activate_integration_schedule(self, plugins): logger.info('Activating plugin tasks') diff --git a/InvenTree/plugin/samples/integration/event_sample.py b/InvenTree/plugin/samples/integration/event_sample.py new file mode 100644 index 0000000000..48516cfa3c --- /dev/null +++ b/InvenTree/plugin/samples/integration/event_sample.py @@ -0,0 +1,29 @@ +""" +Sample plugin which responds to events +""" + +from plugin import IntegrationPluginBase +from plugin.mixins import EventMixin + + +def on_part_saved(*args, **kwargs): + """ + Simple function which responds to a triggered event + """ + + part_id = kwargs['part_id'] + print(f"func on_part_saved() - part: {part_id}") + + +class EventPlugin(EventMixin, IntegrationPluginBase): + """ + A sample plugin which provides supports for triggered events + """ + + PLUGIN_NAME = "EventPlugin" + PLUGIN_SLUG = "event" + PLUGIN_TITLE = "Triggered Events" + + EVENTS = [ + ('part.saved', 'plugin.samples.integration.event_sample.on_part_saved'), + ] diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py index 5a8f866cd7..7a08a21287 100644 --- a/InvenTree/plugin/samples/integration/scheduled_task.py +++ b/InvenTree/plugin/samples/integration/scheduled_task.py @@ -32,7 +32,7 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): 'hello': { 'func': 'plugin.samples.integration.scheduled_task.print_hello', 'schedule': 'I', - 'minutes': 5, + 'minutes': 45, }, 'world': { 'func': 'plugin.samples.integration.scheduled_task.print_hello', diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index 59178300ac..a670d71c34 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -90,7 +90,7 @@ - + {% trans "Installation path" %} {{ plugin.package_path }}