mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 13:28:49 +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()
|
super().save()
|
||||||
|
|
||||||
if self.requires_restart():
|
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:
|
Dict of all global settings values:
|
||||||
@ -978,6 +978,13 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
'requires_restart': True,
|
'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:
|
class Meta:
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
Plugin mixin classes
|
Plugin mixin classes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
|
||||||
@ -9,6 +11,9 @@ from plugin.models import PluginConfig, PluginSetting
|
|||||||
from plugin.urls import PLUGIN_BASE
|
from plugin.urls import PLUGIN_BASE
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
class SettingsMixin:
|
class SettingsMixin:
|
||||||
"""
|
"""
|
||||||
Mixin that enables global settings for the plugin
|
Mixin that enables global settings for the plugin
|
||||||
@ -53,6 +58,128 @@ class SettingsMixin:
|
|||||||
PluginSetting.set_setting(key, value, user, plugin=plugin)
|
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:
|
class UrlsMixin:
|
||||||
"""
|
"""
|
||||||
Mixin that enables custom URLs for the plugin
|
Mixin that enables custom URLs for the plugin
|
||||||
@ -112,7 +239,9 @@ class NavigationMixin:
|
|||||||
NAVIGATION_TAB_ICON = "fas fa-question"
|
NAVIGATION_TAB_ICON = "fas fa-question"
|
||||||
|
|
||||||
class MixinMeta:
|
class MixinMeta:
|
||||||
"""meta options for this mixin"""
|
"""
|
||||||
|
meta options for this mixin
|
||||||
|
"""
|
||||||
MIXIN_NAME = 'Navigation Links'
|
MIXIN_NAME = 'Navigation Links'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -94,6 +94,10 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin):
|
|||||||
def slug(self):
|
def slug(self):
|
||||||
return self.plugin_slug()
|
return self.plugin_slug()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.plugin_name()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def human_name(self):
|
def human_name(self):
|
||||||
"""human readable name for labels etc."""
|
"""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__ = [
|
__all__ = [
|
||||||
'AppMixin',
|
'AppMixin',
|
||||||
'NavigationMixin',
|
'NavigationMixin',
|
||||||
|
'ScheduleMixin',
|
||||||
'SettingsMixin',
|
'SettingsMixin',
|
||||||
'UrlsMixin',
|
'UrlsMixin',
|
||||||
]
|
]
|
||||||
|
@ -262,24 +262,28 @@ 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_schedule(plugins)
|
||||||
self.activate_integration_app(plugins, force_reload=force_reload)
|
self.activate_integration_app(plugins, force_reload=force_reload)
|
||||||
|
|
||||||
def _deactivate_plugins(self):
|
def _deactivate_plugins(self):
|
||||||
"""
|
"""
|
||||||
Run integration deactivation functions for all plugins
|
Run integration deactivation functions for all plugins
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.deactivate_integration_app()
|
self.deactivate_integration_app()
|
||||||
|
self.deactivate_integration_schedule()
|
||||||
self.deactivate_integration_settings()
|
self.deactivate_integration_settings()
|
||||||
|
|
||||||
def activate_integration_settings(self, plugins):
|
def activate_integration_settings(self, plugins):
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
|
|
||||||
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'):
|
logger.info('Activating plugin settings')
|
||||||
logger.info('Registering IntegrationPlugin global settings')
|
|
||||||
for slug, plugin in plugins:
|
self.mixins_settings = {}
|
||||||
if plugin.mixin_enabled('settings'):
|
|
||||||
plugin_setting = plugin.settings
|
for slug, plugin in plugins:
|
||||||
self.mixins_settings[slug] = plugin_setting
|
if plugin.mixin_enabled('settings'):
|
||||||
|
plugin_setting = plugin.settings
|
||||||
|
self.mixins_settings[slug] = plugin_setting
|
||||||
|
|
||||||
def deactivate_integration_settings(self):
|
def deactivate_integration_settings(self):
|
||||||
|
|
||||||
@ -290,10 +294,58 @@ class PluginsRegistry:
|
|||||||
plugin_settings.update(plugin_setting)
|
plugin_settings.update(plugin_setting)
|
||||||
|
|
||||||
# clear cache
|
# 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):
|
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
|
:param plugins: list of IntegrationPlugins that should be installed
|
||||||
:type plugins: dict
|
:type plugins: dict
|
||||||
@ -377,7 +429,10 @@ class PluginsRegistry:
|
|||||||
return plugin_path
|
return plugin_path
|
||||||
|
|
||||||
def deactivate_integration_app(self):
|
def deactivate_integration_app(self):
|
||||||
"""deactivate integration app - some magic required"""
|
"""
|
||||||
|
Deactivate integration app - some magic required
|
||||||
|
"""
|
||||||
|
|
||||||
# unregister models from admin
|
# unregister models from admin
|
||||||
for plugin_path in self.installed_apps:
|
for plugin_path in self.installed_apps:
|
||||||
models = [] # the modelrefs need to be collected as poping an item in a iter is not welcomed
|
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'>
|
<div class='table-responsive'>
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tbody>
|
<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_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_NAVIGATION" icon="fa-sitemap" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP" icon="fa-rocket" %}
|
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP" icon="fa-rocket" %}
|
||||||
@ -28,7 +29,7 @@
|
|||||||
|
|
||||||
<div class='panel-heading'>
|
<div class='panel-heading'>
|
||||||
<div class='d-flex flex-wrap'>
|
<div class='d-flex flex-wrap'>
|
||||||
<h4>{% trans "Plugin list" %}</h4>
|
<h4>{% trans "Plugins" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
{% url 'admin:plugin_pluginconfig_changelist' as url %}
|
{% url 'admin:plugin_pluginconfig_changelist' as url %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user