mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge pull request #2512 from SchrodingersGat/mixins
Adds "scheduled task" mixin for plugins
This commit is contained in:
		| @@ -571,7 +571,7 @@ class InvenTreeSetting(BaseInvenTreeSetting): | ||||
|         super().save() | ||||
|  | ||||
|         if self.requires_restart(): | ||||
|             InvenTreeSetting.set_setting('SERVER_REQUIRES_RESTART', True, None) | ||||
|             InvenTreeSetting.set_setting('SERVER_RESTART_REQUIRED', True, None) | ||||
|  | ||||
|     """ | ||||
|     Dict of all global settings values: | ||||
| @@ -978,6 +978,13 @@ class InvenTreeSetting(BaseInvenTreeSetting): | ||||
|             'validator': bool, | ||||
|             'requires_restart': True, | ||||
|         }, | ||||
|         'ENABLE_PLUGINS_SCHEDULE': { | ||||
|             'name': _('Enable schedule integration'), | ||||
|             'description': _('Enable plugins to run scheduled tasks'), | ||||
|             'default': False, | ||||
|             'validator': bool, | ||||
|             'requires_restart': True, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     class Meta: | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
| Plugin mixin classes | ||||
| """ | ||||
|  | ||||
| import logging | ||||
|  | ||||
| from django.conf.urls import url, include | ||||
| from django.db.utils import OperationalError, ProgrammingError | ||||
|  | ||||
| @@ -9,6 +11,9 @@ from plugin.models import PluginConfig, PluginSetting | ||||
| from plugin.urls import PLUGIN_BASE | ||||
|  | ||||
|  | ||||
| logger = logging.getLogger('inventree') | ||||
|  | ||||
|  | ||||
| class SettingsMixin: | ||||
|     """ | ||||
|     Mixin that enables global settings for the plugin | ||||
| @@ -53,6 +58,128 @@ class SettingsMixin: | ||||
|         PluginSetting.set_setting(key, value, user, plugin=plugin) | ||||
|  | ||||
|  | ||||
| class ScheduleMixin: | ||||
|     """ | ||||
|     Mixin that provides support for scheduled tasks. | ||||
|  | ||||
|     Implementing classes must provide a dict object called SCHEDULED_TASKS, | ||||
|     which provides information on the tasks to be scheduled. | ||||
|  | ||||
|     SCHEDULED_TASKS = { | ||||
|         # Name of the task (will be prepended with the plugin name) | ||||
|         'test_server': { | ||||
|             'func': 'myplugin.tasks.test_server',   # Python function to call (no arguments!) | ||||
|             'schedule': "I",                        # Schedule type (see django_q.Schedule) | ||||
|             'minutes': 30,                          # Number of minutes (only if schedule type = Minutes) | ||||
|             'repeats': 5,                           # Number of repeats (leave blank for 'forever') | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Note: 'schedule' parameter must be one of ['I', 'H', 'D', 'W', 'M', 'Q', 'Y'] | ||||
|     """ | ||||
|  | ||||
|     ALLOWABLE_SCHEDULE_TYPES = ['I', 'H', 'D', 'W', 'M', 'Q', 'Y'] | ||||
|  | ||||
|     SCHEDULED_TASKS = {} | ||||
|  | ||||
|     class MixinMeta: | ||||
|         MIXIN_NAME = 'Schedule' | ||||
|  | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self.add_mixin('schedule', 'has_scheduled_tasks', __class__) | ||||
|         self.scheduled_tasks = getattr(self, 'SCHEDULED_TASKS', {}) | ||||
|  | ||||
|         self.validate_scheduled_tasks() | ||||
|  | ||||
|     @property | ||||
|     def has_scheduled_tasks(self): | ||||
|         return bool(self.scheduled_tasks) | ||||
|  | ||||
|     def validate_scheduled_tasks(self): | ||||
|         """ | ||||
|         Check that the provided scheduled tasks are valid | ||||
|         """ | ||||
|  | ||||
|         if not self.has_scheduled_tasks: | ||||
|             raise ValueError("SCHEDULED_TASKS not defined") | ||||
|  | ||||
|         for key, task in self.scheduled_tasks.items(): | ||||
|  | ||||
|             if 'func' not in task: | ||||
|                 raise ValueError(f"Task '{key}' is missing 'func' parameter") | ||||
|  | ||||
|             if 'schedule' not in task: | ||||
|                 raise ValueError(f"Task '{key}' is missing 'schedule' parameter") | ||||
|  | ||||
|             schedule = task['schedule'].upper().strip() | ||||
|  | ||||
|             if schedule not in self.ALLOWABLE_SCHEDULE_TYPES: | ||||
|                 raise ValueError(f"Task '{key}': Schedule '{schedule}' is not a valid option") | ||||
|  | ||||
|             # If 'minutes' is selected, it must be provided! | ||||
|             if schedule == 'I' and 'minutes' not in task: | ||||
|                 raise ValueError(f"Task '{key}' is missing 'minutes' parameter") | ||||
|  | ||||
|     def get_task_name(self, key): | ||||
|         # Generate a 'unique' task name | ||||
|         slug = self.plugin_slug() | ||||
|         return f"plugin.{slug}.{key}" | ||||
|  | ||||
|     def get_task_names(self): | ||||
|         # Returns a list of all task names associated with this plugin instance | ||||
|         return [self.get_task_name(key) for key in self.scheduled_tasks.keys()] | ||||
|  | ||||
|     def register_tasks(self): | ||||
|         """ | ||||
|         Register the tasks with the database | ||||
|         """ | ||||
|  | ||||
|         try: | ||||
|             from django_q.models import Schedule | ||||
|  | ||||
|             for key, task in self.scheduled_tasks.items(): | ||||
|  | ||||
|                 task_name = self.get_task_name(key) | ||||
|  | ||||
|                 # If a matching scheduled task does not exist, create it! | ||||
|                 if not Schedule.objects.filter(name=task_name).exists(): | ||||
|  | ||||
|                     logger.info(f"Adding scheduled task '{task_name}'") | ||||
|  | ||||
|                     Schedule.objects.create( | ||||
|                         name=task_name, | ||||
|                         func=task['func'], | ||||
|                         schedule_type=task['schedule'], | ||||
|                         minutes=task.get('minutes', None), | ||||
|                         repeats=task.get('repeats', -1), | ||||
|                     ) | ||||
|         except (ProgrammingError, OperationalError): | ||||
|             # Database might not yet be ready | ||||
|             logger.warning("register_tasks failed, database not ready") | ||||
|  | ||||
|     def unregister_tasks(self): | ||||
|         """ | ||||
|         Deregister the tasks with the database | ||||
|         """ | ||||
|  | ||||
|         try: | ||||
|             from django_q.models import Schedule | ||||
|  | ||||
|             for key, task in self.scheduled_tasks.items(): | ||||
|  | ||||
|                 task_name = self.get_task_name(key) | ||||
|  | ||||
|                 try: | ||||
|                     scheduled_task = Schedule.objects.get(name=task_name) | ||||
|                     scheduled_task.delete() | ||||
|                 except Schedule.DoesNotExist: | ||||
|                     pass | ||||
|         except (ProgrammingError, OperationalError): | ||||
|             # Database might not yet be ready | ||||
|             logger.warning("unregister_tasks failed, database not ready") | ||||
|  | ||||
|  | ||||
| class UrlsMixin: | ||||
|     """ | ||||
|     Mixin that enables custom URLs for the plugin | ||||
| @@ -112,7 +239,9 @@ class NavigationMixin: | ||||
|     NAVIGATION_TAB_ICON = "fas fa-question" | ||||
|  | ||||
|     class MixinMeta: | ||||
|         """meta options for this mixin""" | ||||
|         """ | ||||
|         meta options for this mixin | ||||
|         """ | ||||
|         MIXIN_NAME = 'Navigation Links' | ||||
|  | ||||
|     def __init__(self): | ||||
|   | ||||
| @@ -94,6 +94,10 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin): | ||||
|     def slug(self): | ||||
|         return self.plugin_slug() | ||||
|  | ||||
|     @property | ||||
|     def name(self): | ||||
|         return self.plugin_name() | ||||
|  | ||||
|     @property | ||||
|     def human_name(self): | ||||
|         """human readable name for labels etc.""" | ||||
|   | ||||
| @@ -1,9 +1,13 @@ | ||||
| """utility class to enable simpler imports""" | ||||
| from ..builtin.integration.mixins import AppMixin, SettingsMixin, UrlsMixin, NavigationMixin | ||||
| """ | ||||
| Utility class to enable simpler imports | ||||
| """ | ||||
|  | ||||
| from ..builtin.integration.mixins import AppMixin, SettingsMixin, ScheduleMixin, UrlsMixin, NavigationMixin | ||||
|  | ||||
| __all__ = [ | ||||
|     'AppMixin', | ||||
|     'NavigationMixin', | ||||
|     'ScheduleMixin', | ||||
|     'SettingsMixin', | ||||
|     'UrlsMixin', | ||||
| ] | ||||
|   | ||||
| @@ -262,24 +262,28 @@ class PluginsRegistry: | ||||
|         logger.info(f'Found {len(plugins)} active plugins') | ||||
|  | ||||
|         self.activate_integration_settings(plugins) | ||||
|         self.activate_integration_schedule(plugins) | ||||
|         self.activate_integration_app(plugins, force_reload=force_reload) | ||||
|  | ||||
|     def _deactivate_plugins(self): | ||||
|         """ | ||||
|         Run integration deactivation functions for all plugins | ||||
|         """ | ||||
|  | ||||
|         self.deactivate_integration_app() | ||||
|         self.deactivate_integration_schedule() | ||||
|         self.deactivate_integration_settings() | ||||
|  | ||||
|     def activate_integration_settings(self, plugins): | ||||
|         from common.models import InvenTreeSetting | ||||
|  | ||||
|         if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'): | ||||
|             logger.info('Registering IntegrationPlugin global settings') | ||||
|             for slug, plugin in plugins: | ||||
|                 if plugin.mixin_enabled('settings'): | ||||
|                     plugin_setting = plugin.settings | ||||
|                     self.mixins_settings[slug] = plugin_setting | ||||
|         logger.info('Activating plugin settings') | ||||
|  | ||||
|         self.mixins_settings = {} | ||||
|  | ||||
|         for slug, plugin in plugins: | ||||
|             if plugin.mixin_enabled('settings'): | ||||
|                 plugin_setting = plugin.settings | ||||
|                 self.mixins_settings[slug] = plugin_setting | ||||
|  | ||||
|     def deactivate_integration_settings(self): | ||||
|  | ||||
| @@ -290,10 +294,58 @@ class PluginsRegistry: | ||||
|             plugin_settings.update(plugin_setting) | ||||
|  | ||||
|         # clear cache | ||||
|         self.mixins_Fsettings = {} | ||||
|         self.mixins_settings = {} | ||||
|  | ||||
|     def activate_integration_schedule(self, plugins): | ||||
|  | ||||
|         logger.info('Activating plugin tasks') | ||||
|  | ||||
|         from common.models import InvenTreeSetting | ||||
|  | ||||
|         # List of tasks we have activated | ||||
|         task_keys = [] | ||||
|  | ||||
|         if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_SCHEDULE'): | ||||
|  | ||||
|             for slug, plugin in plugins: | ||||
|  | ||||
|                 if plugin.mixin_enabled('schedule'): | ||||
|                     config = plugin.plugin_config() | ||||
|  | ||||
|                     # Only active tasks for plugins which are enabled | ||||
|                     if config and config.active: | ||||
|                         plugin.register_tasks() | ||||
|                         task_keys += plugin.get_task_names() | ||||
|  | ||||
|         if len(task_keys) > 0: | ||||
|             logger.info(f"Activated {len(task_keys)} scheduled tasks") | ||||
|  | ||||
|         # Remove any scheduled tasks which do not match | ||||
|         # This stops 'old' plugin tasks from accumulating | ||||
|         try: | ||||
|             from django_q.models import Schedule | ||||
|  | ||||
|             scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.") | ||||
|  | ||||
|             deleted_count = 0 | ||||
|  | ||||
|             for task in scheduled_plugin_tasks: | ||||
|                 if task.name not in task_keys: | ||||
|                     task.delete() | ||||
|                     deleted_count += 1 | ||||
|  | ||||
|             if deleted_count > 0: | ||||
|                 logger.info(f"Removed {deleted_count} old scheduled tasks") | ||||
|         except (ProgrammingError, OperationalError): | ||||
|             # Database might not yet be ready | ||||
|             logger.warning("activate_integration_schedule failed, database not ready") | ||||
|  | ||||
|     def deactivate_integration_schedule(self): | ||||
|         pass | ||||
|  | ||||
|     def activate_integration_app(self, plugins, force_reload=False): | ||||
|         """activate AppMixin plugins - add custom apps and reload | ||||
|         """ | ||||
|         Activate AppMixin plugins - add custom apps and reload | ||||
|  | ||||
|         :param plugins: list of IntegrationPlugins that should be installed | ||||
|         :type plugins: dict | ||||
| @@ -377,7 +429,10 @@ class PluginsRegistry: | ||||
|         return plugin_path | ||||
|  | ||||
|     def deactivate_integration_app(self): | ||||
|         """deactivate integration app - some magic required""" | ||||
|         """ | ||||
|         Deactivate integration app - some magic required | ||||
|         """ | ||||
|  | ||||
|         # unregister models from admin | ||||
|         for plugin_path in self.installed_apps: | ||||
|             models = []  # the modelrefs need to be collected as poping an item in a iter is not welcomed | ||||
|   | ||||
							
								
								
									
										45
									
								
								InvenTree/plugin/samples/integration/scheduled_task.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								InvenTree/plugin/samples/integration/scheduled_task.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| """ | ||||
| Sample plugin which supports task scheduling | ||||
| """ | ||||
|  | ||||
| from plugin import IntegrationPluginBase | ||||
| from plugin.mixins import ScheduleMixin | ||||
|  | ||||
|  | ||||
| # Define some simple tasks to perform | ||||
| def print_hello(): | ||||
|     print("Hello") | ||||
|  | ||||
|  | ||||
| def print_world(): | ||||
|     print("World") | ||||
|  | ||||
|  | ||||
| def fail_task(): | ||||
|     raise ValueError("This task should fail!") | ||||
|  | ||||
|  | ||||
| class ScheduledTaskPlugin(ScheduleMixin, IntegrationPluginBase): | ||||
|     """ | ||||
|     A sample plugin which provides support for scheduled tasks | ||||
|     """ | ||||
|  | ||||
|     PLUGIN_NAME = "ScheduledTasksPlugin" | ||||
|     PLUGIN_SLUG = "schedule" | ||||
|     PLUGIN_TITLE = "Scheduled Tasks" | ||||
|  | ||||
|     SCHEDULED_TASKS = { | ||||
|         'hello': { | ||||
|             'func': 'plugin.samples.integration.scheduled_task.print_hello', | ||||
|             'schedule': 'I', | ||||
|             'minutes': 5, | ||||
|         }, | ||||
|         'world': { | ||||
|             'func': 'plugin.samples.integration.scheduled_task.print_hello', | ||||
|             'schedule': 'H', | ||||
|         }, | ||||
|         'failure': { | ||||
|             'func': 'plugin.samples.integration.scheduled_task.fail_task', | ||||
|             'schedule': 'D', | ||||
|         }, | ||||
|     } | ||||
| @@ -19,6 +19,7 @@ | ||||
| <div class='table-responsive'> | ||||
| <table class='table table-striped table-condensed'> | ||||
|     <tbody> | ||||
|         {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_SCHEDULE" icon="fa-calendar-alt" %} | ||||
|         {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_URL" icon="fa-link" %} | ||||
|         {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_NAVIGATION" icon="fa-sitemap" %} | ||||
|         {% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP" icon="fa-rocket" %} | ||||
| @@ -28,7 +29,7 @@ | ||||
|  | ||||
| <div class='panel-heading'> | ||||
|     <div class='d-flex flex-wrap'> | ||||
|         <h4>{% trans "Plugin list" %}</h4> | ||||
|         <h4>{% trans "Plugins" %}</h4> | ||||
|         {% include "spacer.html" %} | ||||
|         <div class='btn-group' role='group'> | ||||
|             {% url 'admin:plugin_pluginconfig_changelist' as url %} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user