mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2512 from SchrodingersGat/mixins
Adds "scheduled task" mixin for plugins
This commit is contained in:
commit
31ea7e2792
@ -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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user