Adds sample plugin which responds to triggered events

- Adds some example trigger events for the "Part" model
This commit is contained in:
Oliver 2022-01-08 09:07:27 +11:00
parent a604d85f0f
commit 04d25a60b0
8 changed files with 116 additions and 9 deletions

View File

@ -60,6 +60,8 @@ import common.models
import part.settings as part_settings import part.settings as part_settings
from plugin.events import trigger_event
logger = logging.getLogger("inventree") logger = logging.getLogger("inventree")
@ -1980,10 +1982,10 @@ class Part(MPTTModel):
@property @property
def attachment_count(self): 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, If the part is a variant of a template part,
include the number of attachments for the template part. include the number of attachments for the template part.
""" """
return self.part_attachments.count() 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 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) # Check part stock only if we are *updating* the part (not creating it)
# Run this check in the background # Run this check in the background

View File

@ -189,10 +189,10 @@ class EventMixin:
which provide pairs of 'event':'function' which provide pairs of 'event':'function'
Notes: Notes:
Events are called by name, and based on the django signal nomenclature, Events are called by name, and based on the django signal nomenclature,
e.g. 'part.pre_save' e.g. 'part.pre_save'
Receiving functions must be prototyped to match the 'event' they receive. Receiving functions must be prototyped to match the 'event' they receive.
Example: Example:

View File

@ -8,11 +8,14 @@ from __future__ import unicode_literals
import logging import logging
from django.conf import settings from django.conf import settings
from django.db import transaction
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
from InvenTree.tasks import offload_task from InvenTree.tasks import offload_task
from plugin.registry import plugin_registry
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
@ -38,12 +41,30 @@ def trigger_event(event, *args, **kwargs):
def process_event(event, *args, **kwargs): def process_event(event, *args, **kwargs):
""" """
Respond to a triggered event. Respond to a triggered event.
This function is run by the background worker process. 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}'") logger.info(f"Processing event '{event}'")
# Determine if there are any plugins which are interested in responding # Determine if there are any plugins which are interested in responding
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'): 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
)

View File

@ -63,3 +63,15 @@ class InvenTreePlugin():
raise error raise error
return cfg 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

View File

@ -56,8 +56,10 @@ class PluginsRegistry:
# integration specific # integration specific
self.installed_apps = [] # Holds all added plugin_paths self.installed_apps = [] # Holds all added plugin_paths
# mixins # mixins
self.mixins_settings = {} self.mixins_settings = {}
self.mixins_events = {}
# region public plugin functions # region public plugin functions
def load_plugins(self): def load_plugins(self):
@ -265,6 +267,7 @@ class PluginsRegistry:
logger.info(f'Found {len(plugins)} active plugins') logger.info(f'Found {len(plugins)} active plugins')
self.activate_integration_settings(plugins) self.activate_integration_settings(plugins)
self.activate_integration_events(plugins)
self.activate_integration_schedule(plugins) self.activate_integration_schedule(plugins)
self.activate_integration_app(plugins, force_reload=force_reload) self.activate_integration_app(plugins, force_reload=force_reload)
@ -275,6 +278,7 @@ class PluginsRegistry:
self.deactivate_integration_app() self.deactivate_integration_app()
self.deactivate_integration_schedule() self.deactivate_integration_schedule()
self.deactivate_integration_events()
self.deactivate_integration_settings() self.deactivate_integration_settings()
def activate_integration_settings(self, plugins): def activate_integration_settings(self, plugins):
@ -299,6 +303,41 @@ class PluginsRegistry:
# clear cache # clear cache
self.mixins_settings = {} 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): def activate_integration_schedule(self, plugins):
logger.info('Activating plugin tasks') logger.info('Activating plugin tasks')

View File

@ -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'),
]

View File

@ -32,7 +32,7 @@ class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase):
'hello': { 'hello': {
'func': 'plugin.samples.integration.scheduled_task.print_hello', 'func': 'plugin.samples.integration.scheduled_task.print_hello',
'schedule': 'I', 'schedule': 'I',
'minutes': 5, 'minutes': 45,
}, },
'world': { 'world': {
'func': 'plugin.samples.integration.scheduled_task.print_hello', 'func': 'plugin.samples.integration.scheduled_task.print_hello',

View File

@ -90,7 +90,7 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td></td> <td><span class='fas fa-sitemap'></span></td>
<td>{% trans "Installation path" %}</td> <td>{% trans "Installation path" %}</td>
<td>{{ plugin.package_path }}</td> <td>{{ plugin.package_path }}</td>
</tr> </tr>