mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into plugin-install
This commit is contained in:
@ -1,7 +1,11 @@
|
||||
from .registry import plugins as plugin_reg
|
||||
from .registry import plugin_registry
|
||||
from .plugin import InvenTreePlugin
|
||||
from .integration import IntegrationPluginBase
|
||||
from .action import ActionPlugin
|
||||
|
||||
__all__ = [
|
||||
'plugin_reg', 'IntegrationPluginBase', 'ActionPlugin',
|
||||
'ActionPlugin',
|
||||
'IntegrationPluginBase',
|
||||
'InvenTreePlugin',
|
||||
'plugin_registry',
|
||||
]
|
||||
|
@ -4,43 +4,70 @@ from __future__ import unicode_literals
|
||||
from django.contrib import admin
|
||||
|
||||
import plugin.models as models
|
||||
from plugin import plugin_reg
|
||||
import plugin.registry as registry
|
||||
|
||||
|
||||
def plugin_update(queryset, new_status: bool):
|
||||
"""general function for bulk changing plugins"""
|
||||
"""
|
||||
General function for bulk changing plugins
|
||||
"""
|
||||
|
||||
apps_changed = False
|
||||
|
||||
# run through all plugins in the queryset as the save method needs to be overridden
|
||||
# Run through all plugins in the queryset as the save method needs to be overridden
|
||||
for plugin in queryset:
|
||||
if plugin.active is not new_status:
|
||||
plugin.active = new_status
|
||||
plugin.save(no_reload=True)
|
||||
apps_changed = True
|
||||
|
||||
# reload plugins if they changed
|
||||
# Reload plugins if they changed
|
||||
if apps_changed:
|
||||
plugin_reg.reload_plugins()
|
||||
registry.plugin_registry.reload_plugins()
|
||||
|
||||
|
||||
@admin.action(description='Activate plugin(s)')
|
||||
def plugin_activate(modeladmin, request, queryset):
|
||||
"""activate a set of plugins"""
|
||||
"""
|
||||
Activate a set of plugins
|
||||
"""
|
||||
plugin_update(queryset, True)
|
||||
|
||||
|
||||
@admin.action(description='Deactivate plugin(s)')
|
||||
def plugin_deactivate(modeladmin, request, queryset):
|
||||
"""deactivate a set of plugins"""
|
||||
"""
|
||||
Deactivate a set of plugins
|
||||
"""
|
||||
|
||||
plugin_update(queryset, False)
|
||||
|
||||
|
||||
class PluginSettingInline(admin.TabularInline):
|
||||
"""
|
||||
Inline admin class for PluginSetting
|
||||
"""
|
||||
|
||||
model = models.PluginSetting
|
||||
|
||||
read_only_fields = [
|
||||
'key',
|
||||
]
|
||||
|
||||
def has_add_permission(self, request, obj):
|
||||
return False
|
||||
|
||||
|
||||
class PluginConfigAdmin(admin.ModelAdmin):
|
||||
"""Custom admin with restricted id fields"""
|
||||
"""
|
||||
Custom admin with restricted id fields
|
||||
"""
|
||||
|
||||
readonly_fields = ["key", "name", ]
|
||||
list_display = ['active', '__str__', 'key', 'name', ]
|
||||
list_display = ['name', 'key', '__str__', 'active', ]
|
||||
list_filter = ['active']
|
||||
actions = [plugin_activate, plugin_deactivate, ]
|
||||
inlines = [PluginSettingInline, ]
|
||||
|
||||
|
||||
admin.site.register(models.PluginConfig, PluginConfigAdmin)
|
||||
|
@ -11,7 +11,8 @@ from rest_framework import generics
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
from plugin.models import PluginConfig
|
||||
from common.api import GlobalSettingsPermissions
|
||||
from plugin.models import PluginConfig, PluginSetting
|
||||
import plugin.serializers as PluginSerializers
|
||||
|
||||
|
||||
@ -76,7 +77,46 @@ class PluginInstall(generics.CreateAPIView):
|
||||
return serializer.save()
|
||||
|
||||
|
||||
class PluginSettingList(generics.ListAPIView):
|
||||
"""
|
||||
List endpoint for all plugin related settings.
|
||||
|
||||
- read only
|
||||
- only accessible by staff users
|
||||
"""
|
||||
|
||||
queryset = PluginSetting.objects.all()
|
||||
serializer_class = PluginSerializers.PluginSettingSerializer
|
||||
|
||||
permission_classes = [
|
||||
GlobalSettingsPermissions,
|
||||
]
|
||||
|
||||
|
||||
class PluginSettingDetail(generics.RetrieveUpdateAPIView):
|
||||
"""
|
||||
Detail endpoint for a plugin-specific setting.
|
||||
|
||||
Note that these cannot be created or deleted via the API
|
||||
"""
|
||||
|
||||
queryset = PluginSetting.objects.all()
|
||||
serializer_class = PluginSerializers.PluginSettingSerializer
|
||||
|
||||
# Staff permission required
|
||||
permission_classes = [
|
||||
GlobalSettingsPermissions,
|
||||
]
|
||||
|
||||
|
||||
plugin_api_urls = [
|
||||
|
||||
# Plugin settings URLs
|
||||
url(r'^settings/', include([
|
||||
url(r'^(?P<pk>\d+)/', PluginSettingDetail.as_view(), name='api-plugin-setting-detail'),
|
||||
url(r'^.*$', PluginSettingList.as_view(), name='api-plugin-setting-list'),
|
||||
])),
|
||||
|
||||
# Detail views for a single PluginConfig item
|
||||
url(r'^(?P<pk>\d+)/', include([
|
||||
url(r'^.*$', PluginDetail.as_view(), name='api-plugin-detail'),
|
||||
|
@ -4,17 +4,17 @@ from __future__ import unicode_literals
|
||||
from django.apps import AppConfig
|
||||
from maintenance_mode.core import set_maintenance_mode
|
||||
|
||||
from plugin.registry import plugins
|
||||
from plugin import plugin_registry
|
||||
|
||||
|
||||
class PluginAppConfig(AppConfig):
|
||||
name = 'plugin'
|
||||
|
||||
def ready(self):
|
||||
if not plugins.is_loading:
|
||||
if not plugin_registry.is_loading:
|
||||
# this is the first startup
|
||||
plugins.collect_plugins()
|
||||
plugins.load_plugins()
|
||||
plugin_registry.collect_plugins()
|
||||
plugin_registry.load_plugins()
|
||||
|
||||
# drop out of maintenance
|
||||
# makes sure we did not have an error in reloading and maintenance is still active
|
||||
|
@ -2,11 +2,18 @@
|
||||
Plugin mixin classes
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
import logging
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
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
|
||||
@ -17,44 +24,160 @@ class SettingsMixin:
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_mixin('settings', 'has_globalsettings', __class__)
|
||||
self.globalsettings = getattr(self, 'SETTINGS', None)
|
||||
self.add_mixin('settings', 'has_settings', __class__)
|
||||
self.settings = getattr(self, 'SETTINGS', {})
|
||||
|
||||
@property
|
||||
def has_globalsettings(self):
|
||||
def has_settings(self):
|
||||
"""
|
||||
Does this plugin use custom global settings
|
||||
"""
|
||||
return bool(self.globalsettings)
|
||||
return bool(self.settings)
|
||||
|
||||
def get_setting(self, key):
|
||||
"""
|
||||
Return the 'value' of the setting associated with this plugin
|
||||
"""
|
||||
|
||||
return PluginSetting.get_setting(key, plugin=self)
|
||||
|
||||
def set_setting(self, key, value, user=None):
|
||||
"""
|
||||
Set plugin setting value by key
|
||||
"""
|
||||
|
||||
try:
|
||||
plugin, _ = PluginConfig.objects.get_or_create(key=self.plugin_slug(), name=self.plugin_name())
|
||||
except (OperationalError, ProgrammingError):
|
||||
plugin = None
|
||||
|
||||
if not plugin:
|
||||
# Cannot find associated plugin model, return
|
||||
return
|
||||
|
||||
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 globalsettingspatterns(self):
|
||||
"""
|
||||
Get patterns for InvenTreeSetting defintion
|
||||
"""
|
||||
if self.has_globalsettings:
|
||||
return {f'PLUGIN_{self.slug.upper()}_{key}': value for key, value in self.globalsettings.items()}
|
||||
return None
|
||||
def has_scheduled_tasks(self):
|
||||
return bool(self.scheduled_tasks)
|
||||
|
||||
def _globalsetting_name(self, key):
|
||||
def validate_scheduled_tasks(self):
|
||||
"""
|
||||
Get global name of setting
|
||||
Check that the provided scheduled tasks are valid
|
||||
"""
|
||||
return f'PLUGIN_{self.slug.upper()}_{key}'
|
||||
|
||||
def get_globalsetting(self, key):
|
||||
"""
|
||||
get plugin global setting by key
|
||||
"""
|
||||
from common.models import InvenTreeSetting
|
||||
return InvenTreeSetting.get_setting(self._globalsetting_name(key))
|
||||
if not self.has_scheduled_tasks:
|
||||
raise ValueError("SCHEDULED_TASKS not defined")
|
||||
|
||||
def set_globalsetting(self, key, value, user):
|
||||
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):
|
||||
"""
|
||||
set plugin global setting by key
|
||||
Register the tasks with the database
|
||||
"""
|
||||
from common.models import InvenTreeSetting
|
||||
return InvenTreeSetting.set_setting(self._globalsetting_name(key), value, user)
|
||||
|
||||
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:
|
||||
@ -116,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):
|
||||
|
@ -10,14 +10,14 @@ from django.conf import settings
|
||||
|
||||
# region logging / errors
|
||||
def log_plugin_error(error, reference: str = 'general'):
|
||||
from plugin import plugin_reg
|
||||
from plugin import plugin_registry
|
||||
|
||||
# make sure the registry is set up
|
||||
if reference not in plugin_reg.errors:
|
||||
plugin_reg.errors[reference] = []
|
||||
if reference not in plugin_registry.errors:
|
||||
plugin_registry.errors[reference] = []
|
||||
|
||||
# add error to stack
|
||||
plugin_reg.errors[reference].append(error)
|
||||
plugin_registry.errors[reference].append(error)
|
||||
|
||||
|
||||
class IntegrationPluginError(Exception):
|
||||
|
@ -9,7 +9,6 @@ import pathlib
|
||||
|
||||
from django.urls.base import reverse
|
||||
from django.conf import settings
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import plugin.plugin as plugin
|
||||
@ -20,19 +19,27 @@ logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
class MixinBase:
|
||||
"""general base for mixins"""
|
||||
"""
|
||||
General base for mixins
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._mixinreg = {}
|
||||
self._mixins = {}
|
||||
|
||||
def add_mixin(self, key: str, fnc_enabled=True, cls=None):
|
||||
"""add a mixin to the plugins registry"""
|
||||
"""
|
||||
Add a mixin to the plugins registry
|
||||
"""
|
||||
|
||||
self._mixins[key] = fnc_enabled
|
||||
self.setup_mixin(key, cls=cls)
|
||||
|
||||
def setup_mixin(self, key, cls=None):
|
||||
"""define mixin details for the current mixin -> provides meta details for all active mixins"""
|
||||
"""
|
||||
Define mixin details for the current mixin -> provides meta details for all active mixins
|
||||
"""
|
||||
|
||||
# get human name
|
||||
human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key
|
||||
|
||||
@ -44,7 +51,10 @@ class MixinBase:
|
||||
|
||||
@property
|
||||
def registered_mixins(self, with_base: bool = False):
|
||||
"""get all registered mixins for the plugin"""
|
||||
"""
|
||||
Get all registered mixins for the plugin
|
||||
"""
|
||||
|
||||
mixins = getattr(self, '_mixinreg', None)
|
||||
if mixins:
|
||||
# filter out base
|
||||
@ -59,8 +69,6 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin):
|
||||
"""
|
||||
The IntegrationPluginBase class is used to integrate with 3rd party software
|
||||
"""
|
||||
PLUGIN_SLUG = None
|
||||
PLUGIN_TITLE = None
|
||||
|
||||
AUTHOR = None
|
||||
DESCRIPTION = None
|
||||
@ -84,11 +92,11 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin):
|
||||
# region properties
|
||||
@property
|
||||
def slug(self):
|
||||
"""slug for the plugin"""
|
||||
slug = getattr(self, 'PLUGIN_SLUG', None)
|
||||
if not slug:
|
||||
slug = self.plugin_name()
|
||||
return slugify(slug)
|
||||
return self.plugin_slug()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.plugin_name()
|
||||
|
||||
@property
|
||||
def human_name(self):
|
||||
|
@ -4,7 +4,7 @@ load templates for loaded plugins
|
||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||
from pathlib import Path
|
||||
|
||||
from plugin import plugin_reg
|
||||
from plugin import plugin_registry
|
||||
|
||||
|
||||
class PluginTemplateLoader(FilesystemLoader):
|
||||
@ -12,7 +12,7 @@ class PluginTemplateLoader(FilesystemLoader):
|
||||
def get_dirs(self):
|
||||
dirname = 'templates'
|
||||
template_dirs = []
|
||||
for plugin in plugin_reg.plugins.values():
|
||||
for plugin in plugin_registry.plugins.values():
|
||||
new_path = Path(plugin.path) / dirname
|
||||
if Path(new_path).is_dir():
|
||||
template_dirs.append(new_path)
|
||||
|
26
InvenTree/plugin/migrations/0003_pluginsetting.py
Normal file
26
InvenTree/plugin/migrations/0003_pluginsetting.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.10 on 2022-01-01 10:52
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('plugin', '0002_alter_pluginconfig_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PluginSetting',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(help_text='Settings key (must be unique - case insensitive', max_length=50)),
|
||||
('value', models.CharField(blank=True, help_text='Settings value', max_length=200)),
|
||||
('plugin', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='plugin.pluginconfig', verbose_name='Plugin')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('plugin', 'key')},
|
||||
},
|
||||
),
|
||||
]
|
@ -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',
|
||||
]
|
||||
|
@ -8,16 +8,17 @@ from __future__ import unicode_literals
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
|
||||
from plugin import plugin_reg
|
||||
import common.models
|
||||
|
||||
from plugin import InvenTreePlugin, plugin_registry
|
||||
|
||||
|
||||
class PluginConfig(models.Model):
|
||||
""" A PluginConfig object holds settings for plugins.
|
||||
|
||||
It is used to designate a Part as 'subscribed' for a given User.
|
||||
"""
|
||||
A PluginConfig object holds settings for plugins.
|
||||
|
||||
Attributes:
|
||||
key: slug of the plugin - must be unique
|
||||
key: slug of the plugin (this must be unique across all installed plugins!)
|
||||
name: PluginName of the plugin - serves for a manual double check if the right plugin is used
|
||||
active: Should the plugin be loaded?
|
||||
"""
|
||||
@ -63,12 +64,15 @@ class PluginConfig(models.Model):
|
||||
# functions
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""override to set original state of"""
|
||||
"""
|
||||
Override to set original state of the plugin-config instance
|
||||
"""
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__org_active = self.active
|
||||
|
||||
# append settings from registry
|
||||
self.plugin = plugin_reg.plugins.get(self.key, None)
|
||||
self.plugin = plugin_registry.plugins.get(self.key, None)
|
||||
|
||||
def get_plugin_meta(name):
|
||||
if self.plugin:
|
||||
@ -82,16 +86,112 @@ class PluginConfig(models.Model):
|
||||
}
|
||||
|
||||
def save(self, force_insert=False, force_update=False, *args, **kwargs):
|
||||
"""extend save method to reload plugins if the 'active' status changes"""
|
||||
"""
|
||||
Extend save method to reload plugins if the 'active' status changes
|
||||
"""
|
||||
reload = kwargs.pop('no_reload', False) # check if no_reload flag is set
|
||||
|
||||
ret = super().save(force_insert, force_update, *args, **kwargs)
|
||||
|
||||
if not reload:
|
||||
if self.active is False and self.__org_active is True:
|
||||
plugin_reg.reload_plugins()
|
||||
plugin_registry.reload_plugins()
|
||||
|
||||
elif self.active is True and self.__org_active is False:
|
||||
plugin_reg.reload_plugins()
|
||||
plugin_registry.reload_plugins()
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||
"""
|
||||
This model represents settings for individual plugins
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
unique_together = [
|
||||
('plugin', 'key'),
|
||||
]
|
||||
|
||||
def clean(self, **kwargs):
|
||||
|
||||
kwargs['plugin'] = self.plugin
|
||||
|
||||
super().clean(**kwargs)
|
||||
|
||||
"""
|
||||
We override the following class methods,
|
||||
so that we can pass the plugin instance
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.get_setting_name(self.key, plugin=self.plugin)
|
||||
|
||||
@property
|
||||
def default_value(self):
|
||||
return self.__class__.get_setting_default(self.key, plugin=self.plugin)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.__class__.get_setting_description(self.key, plugin=self.plugin)
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return self.__class__.get_setting_units(self.key, plugin=self.plugin)
|
||||
|
||||
def choices(self):
|
||||
return self.__class__.get_setting_choices(self.key, plugin=self.plugin)
|
||||
|
||||
@classmethod
|
||||
def get_setting_definition(cls, key, **kwargs):
|
||||
"""
|
||||
In the BaseInvenTreeSetting class, we have a class attribute named 'SETTINGS',
|
||||
which is a dict object that fully defines all the setting parameters.
|
||||
|
||||
Here, unlike the BaseInvenTreeSetting, we do not know the definitions of all settings
|
||||
'ahead of time' (as they are defined externally in the plugins).
|
||||
|
||||
Settings can be provided by the caller, as kwargs['settings'].
|
||||
|
||||
If not provided, we'll look at the plugin registry to see what settings are available,
|
||||
(if the plugin is specified!)
|
||||
"""
|
||||
|
||||
if 'settings' not in kwargs:
|
||||
|
||||
plugin = kwargs.pop('plugin', None)
|
||||
|
||||
if plugin:
|
||||
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
plugin = plugin.plugin_config()
|
||||
|
||||
kwargs['settings'] = plugin_registry.mixins_settings.get(plugin.key, {})
|
||||
|
||||
return super().get_setting_definition(key, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_filters(cls, key, **kwargs):
|
||||
"""
|
||||
Override filters method to ensure settings are filtered by plugin id
|
||||
"""
|
||||
|
||||
filters = super().get_filters(key, **kwargs)
|
||||
|
||||
plugin = kwargs.get('plugin', None)
|
||||
|
||||
if plugin:
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
plugin = plugin.plugin_config()
|
||||
filters['plugin'] = plugin
|
||||
|
||||
return filters
|
||||
|
||||
plugin = models.ForeignKey(
|
||||
PluginConfig,
|
||||
related_name='settings',
|
||||
null=False,
|
||||
verbose_name=_('Plugin'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
@ -1,5 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Base Class for InvenTree plugins"""
|
||||
"""
|
||||
Base Class for InvenTree plugins
|
||||
"""
|
||||
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
from django.utils.text import slugify
|
||||
|
||||
|
||||
class InvenTreePlugin():
|
||||
@ -7,12 +12,54 @@ class InvenTreePlugin():
|
||||
Base class for a plugin
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
# Override the plugin name for each concrete plugin instance
|
||||
PLUGIN_NAME = ''
|
||||
|
||||
PLUGIN_SLUG = None
|
||||
|
||||
PLUGIN_TITLE = None
|
||||
|
||||
def plugin_name(self):
|
||||
"""get plugin name"""
|
||||
"""
|
||||
Return the name of this plugin plugin
|
||||
"""
|
||||
return self.PLUGIN_NAME
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
def plugin_slug(self):
|
||||
|
||||
slug = getattr(self, 'PLUGIN_SLUG', None)
|
||||
|
||||
if slug is None:
|
||||
slug = self.plugin_name()
|
||||
|
||||
return slugify(slug.lower())
|
||||
|
||||
def plugin_title(self):
|
||||
|
||||
if self.PLUGIN_TITLE:
|
||||
return self.PLUGIN_TITLE
|
||||
else:
|
||||
return self.plugin_name()
|
||||
|
||||
def plugin_config(self, raise_error=False):
|
||||
"""
|
||||
Return the PluginConfig object associated with this plugin
|
||||
"""
|
||||
|
||||
try:
|
||||
import plugin.models
|
||||
|
||||
cfg, _ = plugin.models.PluginConfig.objects.get_or_create(
|
||||
key=self.plugin_slug(),
|
||||
name=self.plugin_name(),
|
||||
)
|
||||
except (OperationalError, ProgrammingError) as error:
|
||||
cfg = None
|
||||
|
||||
if raise_error:
|
||||
raise error
|
||||
|
||||
return cfg
|
||||
|
@ -1,7 +1,10 @@
|
||||
"""
|
||||
registry for plugins
|
||||
holds the class and the object that contains all code to maintain plugin states
|
||||
Registry for loading and managing multiple plugins at run-time
|
||||
|
||||
- Holds the class and the object that contains all code to maintain plugin states
|
||||
- Manages setup and teardown of plugin class instances
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import pathlib
|
||||
import logging
|
||||
@ -33,7 +36,11 @@ from .helpers import get_plugin_error, IntegrationPluginError
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class Plugins:
|
||||
class PluginsRegistry:
|
||||
"""
|
||||
The PluginsRegistry class
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# plugin registry
|
||||
self.plugins = {}
|
||||
@ -50,15 +57,19 @@ class Plugins:
|
||||
# integration specific
|
||||
self.installed_apps = [] # Holds all added plugin_paths
|
||||
# mixins
|
||||
self.mixins_globalsettings = {}
|
||||
self.mixins_settings = {}
|
||||
|
||||
# region public plugin functions
|
||||
def load_plugins(self):
|
||||
"""load and activate all IntegrationPlugins"""
|
||||
"""
|
||||
Load and activate all IntegrationPlugins
|
||||
"""
|
||||
|
||||
from plugin.helpers import log_plugin_error
|
||||
|
||||
logger.info('Start loading plugins')
|
||||
# set maintanace mode
|
||||
|
||||
# Set maintanace mode
|
||||
_maintenance = bool(get_maintenance_mode())
|
||||
if not _maintenance:
|
||||
set_maintenance_mode(True)
|
||||
@ -68,7 +79,7 @@ class Plugins:
|
||||
retry_counter = settings.PLUGIN_RETRY
|
||||
while not registered_sucessfull:
|
||||
try:
|
||||
# we are using the db so for migrations etc we need to try this block
|
||||
# We are using the db so for migrations etc we need to try this block
|
||||
self._init_plugins(blocked_plugin)
|
||||
self._activate_plugins()
|
||||
registered_sucessfull = True
|
||||
@ -81,13 +92,14 @@ class Plugins:
|
||||
log_plugin_error({error.path: error.message}, 'load')
|
||||
blocked_plugin = error.path # we will not try to load this app again
|
||||
|
||||
# init apps without any integration plugins
|
||||
# Initialize apps without any integration plugins
|
||||
self._clean_registry()
|
||||
self._clean_installed_apps()
|
||||
self._activate_plugins(force_reload=True)
|
||||
|
||||
# we do not want to end in an endless loop
|
||||
# We do not want to end in an endless loop
|
||||
retry_counter -= 1
|
||||
|
||||
if retry_counter <= 0:
|
||||
if settings.PLUGIN_TESTING:
|
||||
print('[PLUGIN] Max retries, breaking loading')
|
||||
@ -98,15 +110,20 @@ class Plugins:
|
||||
|
||||
# now the loading will re-start up with init
|
||||
|
||||
# remove maintenance
|
||||
# Remove maintenance mode
|
||||
if not _maintenance:
|
||||
set_maintenance_mode(False)
|
||||
|
||||
logger.info('Finished loading plugins')
|
||||
|
||||
def unload_plugins(self):
|
||||
"""unload and deactivate all IntegrationPlugins"""
|
||||
"""
|
||||
Unload and deactivate all IntegrationPlugins
|
||||
"""
|
||||
|
||||
logger.info('Start unloading plugins')
|
||||
# set maintanace mode
|
||||
|
||||
# Set maintanace mode
|
||||
_maintenance = bool(get_maintenance_mode())
|
||||
if not _maintenance:
|
||||
set_maintenance_mode(True)
|
||||
@ -123,21 +140,27 @@ class Plugins:
|
||||
logger.info('Finished unloading plugins')
|
||||
|
||||
def reload_plugins(self):
|
||||
"""safely reload IntegrationPlugins"""
|
||||
# do not reload whe currently loading
|
||||
"""
|
||||
Safely reload IntegrationPlugins
|
||||
"""
|
||||
|
||||
# Do not reload whe currently loading
|
||||
if self.is_loading:
|
||||
return
|
||||
|
||||
logger.info('Start reloading plugins')
|
||||
|
||||
with maintenance_mode_on():
|
||||
self.unload_plugins()
|
||||
self.load_plugins()
|
||||
logger.info('Finished reloading plugins')
|
||||
# endregion
|
||||
|
||||
# region general plugin managment mechanisms
|
||||
logger.info('Finished reloading plugins')
|
||||
|
||||
def collect_plugins(self):
|
||||
"""collect integration plugins from all possible ways of loading"""
|
||||
"""
|
||||
Collect integration plugins from all possible ways of loading
|
||||
"""
|
||||
|
||||
self.plugin_modules = [] # clear
|
||||
|
||||
# Collect plugins from paths
|
||||
@ -146,7 +169,7 @@ class Plugins:
|
||||
if modules:
|
||||
[self.plugin_modules.append(item) for item in modules]
|
||||
|
||||
# check if not running in testing mode and apps should be loaded from hooks
|
||||
# Check if not running in testing mode and apps should be loaded from hooks
|
||||
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
|
||||
# Collect plugins from setup entry points
|
||||
for entry in metadata.entry_points().get('inventree_plugins', []):
|
||||
@ -162,22 +185,25 @@ class Plugins:
|
||||
logger.info(", ".join([a.__module__ for a in self.plugin_modules]))
|
||||
|
||||
def _init_plugins(self, disabled=None):
|
||||
"""initialise all found plugins
|
||||
"""
|
||||
Initialise all found plugins
|
||||
|
||||
:param disabled: loading path of disabled app, defaults to None
|
||||
:type disabled: str, optional
|
||||
:raises error: IntegrationPluginError
|
||||
"""
|
||||
|
||||
from plugin.models import PluginConfig
|
||||
|
||||
logger.info('Starting plugin initialisation')
|
||||
|
||||
# Initialize integration plugins
|
||||
for plugin in self.plugin_modules:
|
||||
# check if package
|
||||
# Check if package
|
||||
was_packaged = getattr(plugin, 'is_package', False)
|
||||
|
||||
# check if activated
|
||||
# these checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
|
||||
# Check if activated
|
||||
# These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
|
||||
plug_name = plugin.PLUGIN_NAME
|
||||
plug_key = plugin.PLUGIN_SLUG if getattr(plugin, 'PLUGIN_SLUG', None) else plug_name
|
||||
plug_key = slugify(plug_key) # keys are slugs!
|
||||
@ -189,23 +215,23 @@ class Plugins:
|
||||
raise error
|
||||
plugin_db_setting = None
|
||||
|
||||
# always activate if testing
|
||||
# Always activate if testing
|
||||
if settings.PLUGIN_TESTING or (plugin_db_setting and plugin_db_setting.active):
|
||||
# check if the plugin was blocked -> threw an error
|
||||
# Check if the plugin was blocked -> threw an error
|
||||
if disabled:
|
||||
# option1: package, option2: file-based
|
||||
if (plugin.__name__ == disabled) or (plugin.__module__ == disabled):
|
||||
# errors are bad so disable the plugin in the database
|
||||
# Errors are bad so disable the plugin in the database
|
||||
if not settings.PLUGIN_TESTING:
|
||||
plugin_db_setting.active = False
|
||||
# TODO save the error to the plugin
|
||||
plugin_db_setting.save(no_reload=True)
|
||||
|
||||
# add to inactive plugins so it shows up in the ui
|
||||
# Add to inactive plugins so it shows up in the ui
|
||||
self.plugins_inactive[plug_key] = plugin_db_setting
|
||||
continue # continue -> the plugin is not loaded
|
||||
|
||||
# init package
|
||||
# Initialize package
|
||||
# now we can be sure that an admin has activated the plugin
|
||||
# TODO check more stuff -> as of Nov 2021 there are not many checks in place
|
||||
# but we could enhance those to check signatures, run the plugin against a whitelist etc.
|
||||
@ -228,7 +254,8 @@ class Plugins:
|
||||
self.plugins_inactive[plug_key] = plugin_db_setting
|
||||
|
||||
def _activate_plugins(self, force_reload=False):
|
||||
"""run integration functions for all plugins
|
||||
"""
|
||||
Run integration functions for all plugins
|
||||
|
||||
:param force_reload: force reload base apps, defaults to False
|
||||
:type force_reload: bool, optional
|
||||
@ -237,49 +264,91 @@ class Plugins:
|
||||
plugins = self.plugins.items()
|
||||
logger.info(f'Found {len(plugins)} active plugins')
|
||||
|
||||
self.activate_integration_globalsettings(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"""
|
||||
"""
|
||||
Run integration deactivation functions for all plugins
|
||||
"""
|
||||
|
||||
self.deactivate_integration_app()
|
||||
self.deactivate_integration_globalsettings()
|
||||
# endregion
|
||||
self.deactivate_integration_schedule()
|
||||
self.deactivate_integration_settings()
|
||||
|
||||
# region specific integrations
|
||||
# region integration_globalsettings
|
||||
def activate_integration_globalsettings(self, plugins):
|
||||
from common.models import InvenTreeSetting
|
||||
def activate_integration_settings(self, plugins):
|
||||
|
||||
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.globalsettingspatterns
|
||||
self.mixins_globalsettings[slug] = plugin_setting
|
||||
logger.info('Activating plugin settings')
|
||||
|
||||
# Add to settings dir
|
||||
InvenTreeSetting.GLOBAL_SETTINGS.update(plugin_setting)
|
||||
self.mixins_settings = {}
|
||||
|
||||
def deactivate_integration_globalsettings(self):
|
||||
from common.models import InvenTreeSetting
|
||||
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):
|
||||
|
||||
# collect all settings
|
||||
plugin_settings = {}
|
||||
for _, plugin_setting in self.mixins_globalsettings.items():
|
||||
|
||||
for _, plugin_setting in self.mixins_settings.items():
|
||||
plugin_settings.update(plugin_setting)
|
||||
|
||||
# remove settings
|
||||
for setting in plugin_settings:
|
||||
InvenTreeSetting.GLOBAL_SETTINGS.pop(setting)
|
||||
|
||||
# clear cache
|
||||
self.mixins_globalsettings = {}
|
||||
# endregion
|
||||
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
|
||||
|
||||
# region integration_app
|
||||
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
|
||||
@ -363,7 +432,10 @@ class Plugins:
|
||||
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
|
||||
@ -451,8 +523,6 @@ class Plugins:
|
||||
return True, []
|
||||
except Exception as error:
|
||||
get_plugin_error(error, do_raise=True)
|
||||
# endregion
|
||||
# endregion
|
||||
|
||||
|
||||
plugins = Plugins()
|
||||
plugin_registry = PluginsRegistry()
|
||||
|
@ -44,6 +44,27 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'API_KEY': {
|
||||
'name': _('API Key'),
|
||||
'description': _('Key required for accessing external API'),
|
||||
},
|
||||
'NUMERICAL_SETTING': {
|
||||
'name': _('Numerical'),
|
||||
'description': _('A numerical setting'),
|
||||
'validator': int,
|
||||
'default': 123,
|
||||
},
|
||||
'CHOICE_SETTING': {
|
||||
'name': _("Choice Setting"),
|
||||
'description': _('A setting with multiple choices'),
|
||||
'choices': [
|
||||
('A', 'Anaconda'),
|
||||
('B', 'Bat'),
|
||||
('C', 'Cat'),
|
||||
('D', 'Dog'),
|
||||
],
|
||||
'default': 'A',
|
||||
},
|
||||
}
|
||||
|
||||
NAVIGATION = [
|
||||
|
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',
|
||||
},
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
"""
|
||||
JSON serializers for Stock app
|
||||
JSON serializers for plugin app
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
@ -15,12 +15,14 @@ from django.utils import timezone
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from plugin.models import PluginConfig
|
||||
from plugin.models import PluginConfig, PluginSetting
|
||||
from InvenTree.config import get_plugin_file
|
||||
from common.serializers import SettingsSerializer
|
||||
|
||||
|
||||
class PluginConfigSerializer(serializers.ModelSerializer):
|
||||
""" Serializer for a PluginConfig:
|
||||
"""
|
||||
Serializer for a PluginConfig:
|
||||
"""
|
||||
|
||||
meta = serializers.DictField(read_only=True)
|
||||
@ -73,7 +75,7 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
||||
if not data.get('confirm'):
|
||||
raise ValidationError({'confirm': _('Installation not confirmed')})
|
||||
if (not data.get('url')) and (not data.get('packagename')):
|
||||
msg = _('Either packagenmae of url must be provided')
|
||||
msg = _('Either packagename of URL must be provided')
|
||||
raise ValidationError({'url': msg, 'packagename': msg})
|
||||
|
||||
return data
|
||||
@ -125,3 +127,24 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
||||
plugin_file.write(f'{" ".join(install_name)} # Installed {timezone.now()} by {str(self.context["request"].user)}\n')
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class PluginSettingSerializer(SettingsSerializer):
|
||||
"""
|
||||
Serializer for the PluginSetting model
|
||||
"""
|
||||
|
||||
plugin = serializers.PrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = PluginSetting
|
||||
fields = [
|
||||
'pk',
|
||||
'key',
|
||||
'value',
|
||||
'name',
|
||||
'description',
|
||||
'type',
|
||||
'choices',
|
||||
'plugin',
|
||||
]
|
||||
|
@ -7,7 +7,7 @@ from django import template
|
||||
from django.urls import reverse
|
||||
|
||||
from common.models import InvenTreeSetting
|
||||
from plugin import plugin_reg
|
||||
from plugin import plugin_registry
|
||||
|
||||
|
||||
register = template.Library()
|
||||
@ -16,19 +16,19 @@ register = template.Library()
|
||||
@register.simple_tag()
|
||||
def plugin_list(*args, **kwargs):
|
||||
""" Return a list of all installed integration plugins """
|
||||
return plugin_reg.plugins
|
||||
return plugin_registry.plugins
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def inactive_plugin_list(*args, **kwargs):
|
||||
""" Return a list of all inactive integration plugins """
|
||||
return plugin_reg.plugins_inactive
|
||||
return plugin_registry.plugins_inactive
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def plugin_globalsettings(plugin, *args, **kwargs):
|
||||
""" Return a list of all global settings for a plugin """
|
||||
return plugin_reg.mixins_globalsettings.get(plugin)
|
||||
def plugin_settings(plugin, *args, **kwargs):
|
||||
""" Return a list of all custom settings for a plugin """
|
||||
return plugin_registry.mixins_settings.get(plugin)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
@ -57,4 +57,4 @@ def safe_url(view_name, *args, **kwargs):
|
||||
@register.simple_tag()
|
||||
def plugin_errors(*args, **kwargs):
|
||||
"""Return all plugin errors"""
|
||||
return plugin_reg.errors
|
||||
return plugin_registry.errors
|
||||
|
@ -8,7 +8,7 @@ from InvenTree.api_tester import InvenTreeAPITestCase
|
||||
|
||||
class PluginDetailAPITest(InvenTreeAPITestCase):
|
||||
"""
|
||||
Tests the plugin AP I endpoints
|
||||
Tests the plugin API endpoints
|
||||
"""
|
||||
|
||||
roles = [
|
||||
@ -19,7 +19,7 @@ class PluginDetailAPITest(InvenTreeAPITestCase):
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
self.MSG_NO_PKG = 'Either packagenmae of url must be provided'
|
||||
self.MSG_NO_PKG = 'Either packagename of URL must be provided'
|
||||
|
||||
self.PKG_NAME = 'minimal'
|
||||
super().setUp()
|
||||
@ -64,14 +64,14 @@ class PluginDetailAPITest(InvenTreeAPITestCase):
|
||||
Test the PluginConfig action commands
|
||||
"""
|
||||
from plugin.models import PluginConfig
|
||||
from plugin import plugin_reg
|
||||
from plugin import plugin_registry
|
||||
|
||||
url = reverse('admin:plugin_pluginconfig_changelist')
|
||||
fixtures = PluginConfig.objects.all()
|
||||
|
||||
# check if plugins were registered -> in some test setups the startup has no db access
|
||||
if not fixtures:
|
||||
plugin_reg.reload_plugins()
|
||||
plugin_registry.reload_plugins()
|
||||
fixtures = PluginConfig.objects.all()
|
||||
|
||||
print([str(a) for a in fixtures])
|
||||
|
@ -23,7 +23,7 @@ class BaseMixinDefinition:
|
||||
class SettingsMixinTest(BaseMixinDefinition, TestCase):
|
||||
MIXIN_HUMAN_NAME = 'Settings'
|
||||
MIXIN_NAME = 'settings'
|
||||
MIXIN_ENABLE_CHECK = 'has_globalsettings'
|
||||
MIXIN_ENABLE_CHECK = 'has_settings'
|
||||
|
||||
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
|
||||
|
||||
@ -42,25 +42,19 @@ class SettingsMixinTest(BaseMixinDefinition, TestCase):
|
||||
|
||||
def test_function(self):
|
||||
# settings variable
|
||||
self.assertEqual(self.mixin.globalsettings, self.TEST_SETTINGS)
|
||||
|
||||
# settings pattern
|
||||
target_pattern = {f'PLUGIN_{self.mixin.slug.upper()}_{key}': value for key, value in self.mixin.globalsettings.items()}
|
||||
self.assertEqual(self.mixin.globalsettingspatterns, target_pattern)
|
||||
|
||||
# no settings
|
||||
self.assertIsNone(self.mixin_nothing.globalsettings)
|
||||
self.assertIsNone(self.mixin_nothing.globalsettingspatterns)
|
||||
self.assertEqual(self.mixin.settings, self.TEST_SETTINGS)
|
||||
|
||||
# calling settings
|
||||
# not existing
|
||||
self.assertEqual(self.mixin.get_globalsetting('ABCD'), '')
|
||||
self.assertEqual(self.mixin_nothing.get_globalsetting('ABCD'), '')
|
||||
self.assertEqual(self.mixin.get_setting('ABCD'), '')
|
||||
self.assertEqual(self.mixin_nothing.get_setting('ABCD'), '')
|
||||
|
||||
# right setting
|
||||
self.mixin.set_globalsetting('SETTING1', '12345', self.test_user)
|
||||
self.assertEqual(self.mixin.get_globalsetting('SETTING1'), '12345')
|
||||
self.mixin.set_setting('SETTING1', '12345', self.test_user)
|
||||
self.assertEqual(self.mixin.get_setting('SETTING1'), '12345')
|
||||
|
||||
# no setting
|
||||
self.assertEqual(self.mixin_nothing.get_globalsetting(''), '')
|
||||
self.assertEqual(self.mixin_nothing.get_setting(''), '')
|
||||
|
||||
|
||||
class UrlsMixinTest(BaseMixinDefinition, TestCase):
|
||||
|
@ -1,4 +1,6 @@
|
||||
""" Unit tests for plugins """
|
||||
"""
|
||||
Unit tests for plugins
|
||||
"""
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
@ -6,9 +8,8 @@ import plugin.plugin
|
||||
import plugin.integration
|
||||
from plugin.samples.integration.sample import SampleIntegrationPlugin
|
||||
from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin
|
||||
# from plugin.plugins import load_action_plugins, load_barcode_plugins
|
||||
import plugin.templatetags.plugin_extras as plugin_tags
|
||||
from plugin import plugin_reg
|
||||
from plugin import plugin_registry
|
||||
|
||||
|
||||
class InvenTreePluginTests(TestCase):
|
||||
@ -57,17 +58,17 @@ class PluginTagTests(TestCase):
|
||||
|
||||
def test_tag_plugin_list(self):
|
||||
"""test that all plugins are listed"""
|
||||
self.assertEqual(plugin_tags.plugin_list(), plugin_reg.plugins)
|
||||
self.assertEqual(plugin_tags.plugin_list(), plugin_registry.plugins)
|
||||
|
||||
def test_tag_incative_plugin_list(self):
|
||||
"""test that all inactive plugins are listed"""
|
||||
self.assertEqual(plugin_tags.inactive_plugin_list(), plugin_reg.plugins_inactive)
|
||||
self.assertEqual(plugin_tags.inactive_plugin_list(), plugin_registry.plugins_inactive)
|
||||
|
||||
def test_tag_plugin_globalsettings(self):
|
||||
def test_tag_plugin_settings(self):
|
||||
"""check all plugins are listed"""
|
||||
self.assertEqual(
|
||||
plugin_tags.plugin_globalsettings(self.sample),
|
||||
plugin_reg.mixins_globalsettings.get(self.sample)
|
||||
plugin_tags.plugin_settings(self.sample),
|
||||
plugin_registry.mixins_settings.get(self.sample)
|
||||
)
|
||||
|
||||
def test_tag_mixin_enabled(self):
|
||||
@ -89,4 +90,4 @@ class PluginTagTests(TestCase):
|
||||
|
||||
def test_tag_plugin_errors(self):
|
||||
"""test that all errors are listed"""
|
||||
self.assertEqual(plugin_tags.plugin_errors(), plugin_reg.errors)
|
||||
self.assertEqual(plugin_tags.plugin_errors(), plugin_registry.errors)
|
||||
|
@ -1,18 +1,24 @@
|
||||
"""
|
||||
URL lookup for plugin app
|
||||
"""
|
||||
|
||||
from django.conf.urls import url, include
|
||||
|
||||
from plugin import plugin_reg
|
||||
from plugin import plugin_registry
|
||||
|
||||
|
||||
PLUGIN_BASE = 'plugin' # Constant for links
|
||||
|
||||
|
||||
def get_plugin_urls():
|
||||
"""returns a urlpattern that can be integrated into the global urls"""
|
||||
"""
|
||||
Returns a urlpattern that can be integrated into the global urls
|
||||
"""
|
||||
|
||||
urls = []
|
||||
for plugin in plugin_reg.plugins.values():
|
||||
|
||||
for plugin in plugin_registry.plugins.values():
|
||||
if plugin.mixin_enabled('urls'):
|
||||
urls.append(plugin.urlpatterns)
|
||||
|
||||
return url(f'^{PLUGIN_BASE}/', include((urls, 'plugin')))
|
||||
|
Reference in New Issue
Block a user