"""Functions for triggering and responding to server side events.""" import logging from django.conf import settings from django.db import transaction from django.db.models.signals import post_delete, post_save from django.dispatch.dispatcher import receiver from InvenTree.ready import canAppAccessDatabase, isImportingData from InvenTree.tasks import offload_task from plugin.registry import registry logger = logging.getLogger('inventree') def trigger_event(event, *args, **kwargs): """Trigger an event with optional arguments. This event will be stored in the database, and the worker will respond to it later on. """ from common.models import InvenTreeSetting if not settings.PLUGINS_ENABLED: # Do nothing if plugins are not enabled return # pragma: no cover if not InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS', False): # Do nothing if plugin events are not enabled return # Make sure the database can be accessed and is not being tested rn if not canAppAccessDatabase(allow_shell=True) and not settings.PLUGIN_TESTING_EVENTS: logger.debug("Ignoring triggered event '%s' - database not ready", event) return logger.debug("Event triggered: '%s'", event) # By default, force the event to be processed asynchronously if 'force_async' not in kwargs and not settings.PLUGIN_TESTING_EVENTS: kwargs['force_async'] = True offload_task( register_event, event, *args, **kwargs ) def register_event(event, *args, **kwargs): """Register the event with any interested plugins. Note: This function is processed by the background worker, as it performs multiple database access operations. """ from common.models import InvenTreeSetting logger.debug("Registering triggered event: '%s'", event) # Determine if there are any plugins which are interested in responding if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'): with transaction.atomic(): for slug, plugin in registry.plugins.items(): if not plugin.mixin_enabled('events'): continue # Only allow event registering for 'active' plugins if not plugin.is_active(): continue # 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) # 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 ) def process_event(plugin_slug, 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. """ plugin = registry.get_plugin(plugin_slug) if plugin is None: # pragma: no cover logger.error("Could not find matching plugin for '%s'", plugin_slug) return plugin.process_event(event, *args, **kwargs) logger.debug("Plugin '%s' is processing triggered event '%s'", plugin_slug, event) def allow_table_event(table_name): """Determine if an automatic event should be fired for a given table. We *do not* want events to be fired for some tables! """ # Prevent table events during the data import process if isImportingData(): return False # pragma: no cover # Prevent table events when in testing mode (saves a lot of time) if settings.TESTING: return False table_name = table_name.lower().strip() # Ignore any tables which start with these prefixes ignore_prefixes = [ 'account_', 'auth_', 'authtoken_', 'django_', 'error_', 'exchange_', 'otp_', 'plugin_', 'socialaccount_', 'user_', 'users_', ] if any(table_name.startswith(prefix) for prefix in ignore_prefixes): return False ignore_tables = [ 'common_notificationentry', 'common_notificationmessage', 'common_webhookendpoint', 'common_webhookmessage', 'part_partpricing', 'part_partstocktake', 'part_partstocktakereport', ] if table_name in ignore_tables: return False return True @receiver(post_save) def after_save(sender, instance, created, **kwargs): """Trigger an event whenever a database entry is saved.""" table = sender.objects.model._meta.db_table instance_id = getattr(instance, 'id', None) if instance_id is None: return if not allow_table_event(table): return if created: trigger_event( f'{table}.created', id=instance.id, model=sender.__name__, ) else: trigger_event( f'{table}.saved', id=instance.id, model=sender.__name__, ) @receiver(post_delete) def after_delete(sender, instance, **kwargs): """Trigger an event whenever a database entry is deleted.""" table = sender.objects.model._meta.db_table if not allow_table_event(table): return trigger_event( f'{table}.deleted', model=sender.__name__, )