mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-04 06:18:48 +00:00
Merge pull request #2492 from SchrodingersGat/plugin-settings-refactor
Plugin settings refactor
This commit is contained in:
commit
7967c0c0bd
@ -2,9 +2,14 @@
|
|||||||
Custom management command to cleanup old settings that are not defined anymore
|
Custom management command to cleanup old settings that are not defined anymore
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""
|
"""
|
||||||
Cleanup old (undefined) settings in the database
|
Cleanup old (undefined) settings in the database
|
||||||
@ -12,27 +17,27 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args, **kwargs):
|
||||||
|
|
||||||
print("Collecting settings")
|
logger.info("Collecting settings")
|
||||||
from common.models import InvenTreeSetting, InvenTreeUserSetting
|
from common.models import InvenTreeSetting, InvenTreeUserSetting
|
||||||
|
|
||||||
# general settings
|
# general settings
|
||||||
db_settings = InvenTreeSetting.objects.all()
|
db_settings = InvenTreeSetting.objects.all()
|
||||||
model_settings = InvenTreeSetting.GLOBAL_SETTINGS
|
model_settings = InvenTreeSetting.SETTINGS
|
||||||
|
|
||||||
# check if key exist and delete if not
|
# check if key exist and delete if not
|
||||||
for setting in db_settings:
|
for setting in db_settings:
|
||||||
if setting.key not in model_settings:
|
if setting.key not in model_settings:
|
||||||
setting.delete()
|
setting.delete()
|
||||||
print(f"deleted setting '{setting.key}'")
|
logger.info(f"deleted setting '{setting.key}'")
|
||||||
|
|
||||||
# user settings
|
# user settings
|
||||||
db_settings = InvenTreeUserSetting.objects.all()
|
db_settings = InvenTreeUserSetting.objects.all()
|
||||||
model_settings = InvenTreeUserSetting.GLOBAL_SETTINGS
|
model_settings = InvenTreeUserSetting.SETTINGS
|
||||||
|
|
||||||
# check if key exist and delete if not
|
# check if key exist and delete if not
|
||||||
for setting in db_settings:
|
for setting in db_settings:
|
||||||
if setting.key not in model_settings:
|
if setting.key not in model_settings:
|
||||||
setting.delete()
|
setting.delete()
|
||||||
print(f"deleted user setting '{setting.key}'")
|
logger.info(f"deleted user setting '{setting.key}'")
|
||||||
|
|
||||||
print("checked all settings")
|
logger.info("checked all settings")
|
||||||
|
@ -12,11 +12,15 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 22
|
INVENTREE_API_VERSION = 23
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v23 -> 2022-02-02
|
||||||
|
- Adds API endpoints for managing plugin classes
|
||||||
|
- Adds API endpoints for managing plugin settings
|
||||||
|
|
||||||
v22 -> 2021-12-20
|
v22 -> 2021-12-20
|
||||||
- Adds API endpoint to "merge" multiple stock items
|
- Adds API endpoint to "merge" multiple stock items
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
single values (e.g. one-off settings values).
|
single values (e.g. one-off settings values).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GLOBAL_SETTINGS = {}
|
SETTINGS = {}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
@ -65,7 +65,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
self.key = str(self.key).upper()
|
self.key = str(self.key).upper()
|
||||||
|
|
||||||
self.clean()
|
self.clean(**kwargs)
|
||||||
self.validate_unique()
|
self.validate_unique()
|
||||||
|
|
||||||
super().save()
|
super().save()
|
||||||
@ -82,6 +82,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
results = cls.objects.all()
|
results = cls.objects.all()
|
||||||
|
|
||||||
|
# Optionally filter by user
|
||||||
if user is not None:
|
if user is not None:
|
||||||
results = results.filter(user=user)
|
results = results.filter(user=user)
|
||||||
|
|
||||||
@ -93,13 +94,13 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
settings[setting.key.upper()] = setting.value
|
settings[setting.key.upper()] = setting.value
|
||||||
|
|
||||||
# Specify any "default" values which are not in the database
|
# Specify any "default" values which are not in the database
|
||||||
for key in cls.GLOBAL_SETTINGS.keys():
|
for key in cls.SETTINGS.keys():
|
||||||
|
|
||||||
if key.upper() not in settings:
|
if key.upper() not in settings:
|
||||||
settings[key.upper()] = cls.get_setting_default(key)
|
settings[key.upper()] = cls.get_setting_default(key)
|
||||||
|
|
||||||
if exclude_hidden:
|
if exclude_hidden:
|
||||||
hidden = cls.GLOBAL_SETTINGS[key].get('hidden', False)
|
hidden = cls.SETTINGS[key].get('hidden', False)
|
||||||
|
|
||||||
if hidden:
|
if hidden:
|
||||||
# Remove hidden items
|
# Remove hidden items
|
||||||
@ -123,98 +124,92 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
return settings
|
return settings
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_name(cls, key):
|
def get_setting_definition(cls, key, **kwargs):
|
||||||
|
"""
|
||||||
|
Return the 'definition' of a particular settings value, as a dict object.
|
||||||
|
|
||||||
|
- The 'settings' dict can be passed as a kwarg
|
||||||
|
- If not passed, look for cls.SETTINGS
|
||||||
|
- Returns an empty dict if the key is not found
|
||||||
|
"""
|
||||||
|
|
||||||
|
settings = kwargs.get('settings', cls.SETTINGS)
|
||||||
|
|
||||||
|
key = str(key).strip().upper()
|
||||||
|
|
||||||
|
if settings is not None and key in settings:
|
||||||
|
return settings[key]
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_setting_name(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the name of a particular setting.
|
Return the name of a particular setting.
|
||||||
|
|
||||||
If it does not exist, return an empty string.
|
If it does not exist, return an empty string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = str(key).strip().upper()
|
setting = cls.get_setting_definition(key, **kwargs)
|
||||||
|
return setting.get('name', '')
|
||||||
if key in cls.GLOBAL_SETTINGS:
|
|
||||||
setting = cls.GLOBAL_SETTINGS[key]
|
|
||||||
return setting.get('name', '')
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_description(cls, key):
|
def get_setting_description(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the description for a particular setting.
|
Return the description for a particular setting.
|
||||||
|
|
||||||
If it does not exist, return an empty string.
|
If it does not exist, return an empty string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = str(key).strip().upper()
|
setting = cls.get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
if key in cls.GLOBAL_SETTINGS:
|
return setting.get('description', '')
|
||||||
setting = cls.GLOBAL_SETTINGS[key]
|
|
||||||
return setting.get('description', '')
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_units(cls, key):
|
def get_setting_units(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the units for a particular setting.
|
Return the units for a particular setting.
|
||||||
|
|
||||||
If it does not exist, return an empty string.
|
If it does not exist, return an empty string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = str(key).strip().upper()
|
setting = cls.get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
if key in cls.GLOBAL_SETTINGS:
|
return setting.get('units', '')
|
||||||
setting = cls.GLOBAL_SETTINGS[key]
|
|
||||||
return setting.get('units', '')
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_validator(cls, key):
|
def get_setting_validator(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the validator for a particular setting.
|
Return the validator for a particular setting.
|
||||||
|
|
||||||
If it does not exist, return None
|
If it does not exist, return None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = str(key).strip().upper()
|
setting = cls.get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
if key in cls.GLOBAL_SETTINGS:
|
return setting.get('validator', None)
|
||||||
setting = cls.GLOBAL_SETTINGS[key]
|
|
||||||
return setting.get('validator', None)
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_default(cls, key):
|
def get_setting_default(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the default value for a particular setting.
|
Return the default value for a particular setting.
|
||||||
|
|
||||||
If it does not exist, return an empty string
|
If it does not exist, return an empty string
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = str(key).strip().upper()
|
setting = cls.get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
if key in cls.GLOBAL_SETTINGS:
|
return setting.get('default', '')
|
||||||
setting = cls.GLOBAL_SETTINGS[key]
|
|
||||||
return setting.get('default', '')
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_choices(cls, key):
|
def get_setting_choices(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the validator choices available for a particular setting.
|
Return the validator choices available for a particular setting.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = str(key).strip().upper()
|
setting = cls.get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
if key in cls.GLOBAL_SETTINGS:
|
choices = setting.get('choices', None)
|
||||||
setting = cls.GLOBAL_SETTINGS[key]
|
|
||||||
choices = setting.get('choices', None)
|
|
||||||
else:
|
|
||||||
choices = None
|
|
||||||
|
|
||||||
if callable(choices):
|
if callable(choices):
|
||||||
# Evaluate the function (we expect it will return a list of tuples...)
|
# Evaluate the function (we expect it will return a list of tuples...)
|
||||||
@ -237,17 +232,40 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
key = str(key).strip().upper()
|
key = str(key).strip().upper()
|
||||||
|
|
||||||
|
settings = cls.objects.all()
|
||||||
|
|
||||||
|
# Filter by user
|
||||||
|
user = kwargs.get('user', None)
|
||||||
|
|
||||||
|
if user is not None:
|
||||||
|
settings = settings.filter(user=user)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
setting = cls.objects.filter(**cls.get_filters(key, **kwargs)).first()
|
setting = settings.filter(**cls.get_filters(key, **kwargs)).first()
|
||||||
except (ValueError, cls.DoesNotExist):
|
except (ValueError, cls.DoesNotExist):
|
||||||
setting = None
|
setting = None
|
||||||
except (IntegrityError, OperationalError):
|
except (IntegrityError, OperationalError):
|
||||||
setting = None
|
setting = None
|
||||||
|
|
||||||
|
plugin = kwargs.pop('plugin', None)
|
||||||
|
|
||||||
|
if plugin:
|
||||||
|
from plugin import InvenTreePlugin
|
||||||
|
|
||||||
|
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||||
|
plugin = plugin.plugin_config()
|
||||||
|
|
||||||
|
kwargs['plugin'] = plugin
|
||||||
|
|
||||||
# Setting does not exist! (Try to create it)
|
# Setting does not exist! (Try to create it)
|
||||||
if not setting:
|
if not setting:
|
||||||
|
|
||||||
setting = cls(key=key, value=cls.get_setting_default(key), **kwargs)
|
# Attempt to create a new settings object
|
||||||
|
setting = cls(
|
||||||
|
key=key,
|
||||||
|
value=cls.get_setting_default(key, **kwargs),
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Wrap this statement in "atomic", so it can be rolled back if it fails
|
# Wrap this statement in "atomic", so it can be rolled back if it fails
|
||||||
@ -259,21 +277,6 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return setting
|
return setting
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_setting_pk(cls, key):
|
|
||||||
"""
|
|
||||||
Return the primary-key value for a given setting.
|
|
||||||
|
|
||||||
If the setting does not exist, return None
|
|
||||||
"""
|
|
||||||
|
|
||||||
setting = cls.get_setting_object(cls)
|
|
||||||
|
|
||||||
if setting:
|
|
||||||
return setting.pk
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting(cls, key, backup_value=None, **kwargs):
|
def get_setting(cls, key, backup_value=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -283,18 +286,19 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
# If no backup value is specified, atttempt to retrieve a "default" value
|
# If no backup value is specified, atttempt to retrieve a "default" value
|
||||||
if backup_value is None:
|
if backup_value is None:
|
||||||
backup_value = cls.get_setting_default(key)
|
backup_value = cls.get_setting_default(key, **kwargs)
|
||||||
|
|
||||||
setting = cls.get_setting_object(key, **kwargs)
|
setting = cls.get_setting_object(key, **kwargs)
|
||||||
|
|
||||||
if setting:
|
if setting:
|
||||||
value = setting.value
|
value = setting.value
|
||||||
|
|
||||||
# If the particular setting is defined as a boolean, cast the value to a boolean
|
# Cast to boolean if necessary
|
||||||
if setting.is_bool():
|
if setting.is_bool(**kwargs):
|
||||||
value = InvenTree.helpers.str2bool(value)
|
value = InvenTree.helpers.str2bool(value)
|
||||||
|
|
||||||
if setting.is_int():
|
# Cast to integer if necessary
|
||||||
|
if setting.is_int(**kwargs):
|
||||||
try:
|
try:
|
||||||
value = int(value)
|
value = int(value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
@ -357,7 +361,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
def units(self):
|
def units(self):
|
||||||
return self.__class__.get_setting_units(self.key)
|
return self.__class__.get_setting_units(self.key)
|
||||||
|
|
||||||
def clean(self):
|
def clean(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
If a validator (or multiple validators) are defined for a particular setting key,
|
If a validator (or multiple validators) are defined for a particular setting key,
|
||||||
run them against the 'value' field.
|
run them against the 'value' field.
|
||||||
@ -365,25 +369,16 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
validator = self.__class__.get_setting_validator(self.key)
|
validator = self.__class__.get_setting_validator(self.key, **kwargs)
|
||||||
|
|
||||||
if self.is_bool():
|
if validator is not None:
|
||||||
self.value = InvenTree.helpers.str2bool(self.value)
|
self.run_validator(validator)
|
||||||
|
|
||||||
if self.is_int():
|
|
||||||
try:
|
|
||||||
self.value = int(self.value)
|
|
||||||
except (ValueError):
|
|
||||||
raise ValidationError(_('Must be an integer value'))
|
|
||||||
|
|
||||||
options = self.valid_options()
|
options = self.valid_options()
|
||||||
|
|
||||||
if options and self.value not in options:
|
if options and self.value not in options:
|
||||||
raise ValidationError(_("Chosen value is not a valid option"))
|
raise ValidationError(_("Chosen value is not a valid option"))
|
||||||
|
|
||||||
if validator is not None:
|
|
||||||
self.run_validator(validator)
|
|
||||||
|
|
||||||
def run_validator(self, validator):
|
def run_validator(self, validator):
|
||||||
"""
|
"""
|
||||||
Run a validator against the 'value' field for this InvenTreeSetting object.
|
Run a validator against the 'value' field for this InvenTreeSetting object.
|
||||||
@ -395,7 +390,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
value = self.value
|
value = self.value
|
||||||
|
|
||||||
# Boolean validator
|
# Boolean validator
|
||||||
if self.is_bool():
|
if validator is bool:
|
||||||
# Value must "look like" a boolean value
|
# Value must "look like" a boolean value
|
||||||
if InvenTree.helpers.is_bool(value):
|
if InvenTree.helpers.is_bool(value):
|
||||||
# Coerce into either "True" or "False"
|
# Coerce into either "True" or "False"
|
||||||
@ -406,7 +401,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Integer validator
|
# Integer validator
|
||||||
if self.is_int():
|
if validator is int:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Coerce into an integer value
|
# Coerce into an integer value
|
||||||
@ -459,12 +454,12 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return [opt[0] for opt in choices]
|
return [opt[0] for opt in choices]
|
||||||
|
|
||||||
def is_bool(self):
|
def is_bool(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Check if this setting is required to be a boolean value
|
Check if this setting is required to be a boolean value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
validator = self.__class__.get_setting_validator(self.key)
|
validator = self.__class__.get_setting_validator(self.key, **kwargs)
|
||||||
|
|
||||||
return self.__class__.validator_is_bool(validator)
|
return self.__class__.validator_is_bool(validator)
|
||||||
|
|
||||||
@ -477,15 +472,15 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return InvenTree.helpers.str2bool(self.value)
|
return InvenTree.helpers.str2bool(self.value)
|
||||||
|
|
||||||
def setting_type(self):
|
def setting_type(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Return the field type identifier for this setting object
|
Return the field type identifier for this setting object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.is_bool():
|
if self.is_bool(**kwargs):
|
||||||
return 'boolean'
|
return 'boolean'
|
||||||
|
|
||||||
elif self.is_int():
|
elif self.is_int(**kwargs):
|
||||||
return 'integer'
|
return 'integer'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -504,12 +499,12 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_int(self):
|
def is_int(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Check if the setting is required to be an integer value:
|
Check if the setting is required to be an integer value:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
validator = self.__class__.get_setting_validator(self.key)
|
validator = self.__class__.get_setting_validator(self.key, **kwargs)
|
||||||
|
|
||||||
return self.__class__.validator_is_int(validator)
|
return self.__class__.validator_is_int(validator)
|
||||||
|
|
||||||
@ -541,21 +536,20 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_protected(cls, key):
|
def is_protected(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Check if the setting value is protected
|
Check if the setting value is protected
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = str(key).strip().upper()
|
setting = cls.get_setting_definition(key, **kwargs)
|
||||||
|
|
||||||
if key in cls.GLOBAL_SETTINGS:
|
return setting.get('protected', False)
|
||||||
return cls.GLOBAL_SETTINGS[key].get('protected', False)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def settings_group_options():
|
def settings_group_options():
|
||||||
"""build up group tuple for settings based on gour choices"""
|
"""
|
||||||
|
Build up group tuple for settings based on your choices
|
||||||
|
"""
|
||||||
return [('', _('No group')), *[(str(a.id), str(a)) for a in Group.objects.all()]]
|
return [('', _('No group')), *[(str(a.id), str(a)) for a in Group.objects.all()]]
|
||||||
|
|
||||||
|
|
||||||
@ -595,7 +589,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
The keys must be upper-case
|
The keys must be upper-case
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GLOBAL_SETTINGS = {
|
SETTINGS = {
|
||||||
|
|
||||||
'SERVER_RESTART_REQUIRED': {
|
'SERVER_RESTART_REQUIRED': {
|
||||||
'name': _('Restart required'),
|
'name': _('Restart required'),
|
||||||
@ -977,13 +971,6 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
'requires_restart': True,
|
'requires_restart': True,
|
||||||
},
|
},
|
||||||
'ENABLE_PLUGINS_GLOBALSETTING': {
|
|
||||||
'name': _('Enable global setting integration'),
|
|
||||||
'description': _('Enable plugins to integrate into inventree global settings'),
|
|
||||||
'default': False,
|
|
||||||
'validator': bool,
|
|
||||||
'requires_restart': True,
|
|
||||||
},
|
|
||||||
'ENABLE_PLUGINS_APP': {
|
'ENABLE_PLUGINS_APP': {
|
||||||
'name': _('Enable app integration'),
|
'name': _('Enable app integration'),
|
||||||
'description': _('Enable plugins to add apps'),
|
'description': _('Enable plugins to add apps'),
|
||||||
@ -1017,7 +1004,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
Return True if this setting requires a server restart after changing
|
Return True if this setting requires a server restart after changing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
options = InvenTreeSetting.GLOBAL_SETTINGS.get(self.key, None)
|
options = InvenTreeSetting.SETTINGS.get(self.key, None)
|
||||||
|
|
||||||
if options:
|
if options:
|
||||||
return options.get('requires_restart', False)
|
return options.get('requires_restart', False)
|
||||||
@ -1030,7 +1017,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
An InvenTreeSetting object with a usercontext
|
An InvenTreeSetting object with a usercontext
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GLOBAL_SETTINGS = {
|
SETTINGS = {
|
||||||
'HOMEPAGE_PART_STARRED': {
|
'HOMEPAGE_PART_STARRED': {
|
||||||
'name': _('Show subscribed parts'),
|
'name': _('Show subscribed parts'),
|
||||||
'description': _('Show subscribed parts on the homepage'),
|
'description': _('Show subscribed parts on the homepage'),
|
||||||
|
@ -49,9 +49,9 @@ class SettingsTest(TestCase):
|
|||||||
- Ensure that every global setting has a description.
|
- Ensure that every global setting has a description.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for key in InvenTreeSetting.GLOBAL_SETTINGS.keys():
|
for key in InvenTreeSetting.SETTINGS.keys():
|
||||||
|
|
||||||
setting = InvenTreeSetting.GLOBAL_SETTINGS[key]
|
setting = InvenTreeSetting.SETTINGS[key]
|
||||||
|
|
||||||
name = setting.get('name', None)
|
name = setting.get('name', None)
|
||||||
|
|
||||||
@ -64,14 +64,14 @@ class SettingsTest(TestCase):
|
|||||||
raise ValueError(f'Missing GLOBAL_SETTING description for {key}')
|
raise ValueError(f'Missing GLOBAL_SETTING description for {key}')
|
||||||
|
|
||||||
if not key == key.upper():
|
if not key == key.upper():
|
||||||
raise ValueError(f"GLOBAL_SETTINGS key '{key}' is not uppercase")
|
raise ValueError(f"SETTINGS key '{key}' is not uppercase")
|
||||||
|
|
||||||
def test_defaults(self):
|
def test_defaults(self):
|
||||||
"""
|
"""
|
||||||
Populate the settings with default values
|
Populate the settings with default values
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for key in InvenTreeSetting.GLOBAL_SETTINGS.keys():
|
for key in InvenTreeSetting.SETTINGS.keys():
|
||||||
|
|
||||||
value = InvenTreeSetting.get_setting_default(key)
|
value = InvenTreeSetting.get_setting_default(key)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
""" This module provides template tags for extra functionality
|
"""
|
||||||
|
This module provides template tags for extra functionality,
|
||||||
over and above the built-in Django tags.
|
over and above the built-in Django tags.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -22,6 +23,8 @@ import InvenTree.helpers
|
|||||||
from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting
|
from common.models import InvenTreeSetting, ColorTheme, InvenTreeUserSetting
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
|
|
||||||
|
from plugin.models import PluginSetting
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@ -223,8 +226,16 @@ def setting_object(key, *args, **kwargs):
|
|||||||
if a user-setting was requested return that
|
if a user-setting was requested return that
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if 'plugin' in kwargs:
|
||||||
|
# Note, 'plugin' is an instance of an InvenTreePlugin class
|
||||||
|
|
||||||
|
plugin = kwargs['plugin']
|
||||||
|
|
||||||
|
return PluginSetting.get_setting_object(key, plugin=plugin)
|
||||||
|
|
||||||
if 'user' in kwargs:
|
if 'user' in kwargs:
|
||||||
return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'])
|
return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'])
|
||||||
|
|
||||||
return InvenTreeSetting.get_setting_object(key)
|
return InvenTreeSetting.get_setting_object(key)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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 .integration import IntegrationPluginBase
|
||||||
from .action import ActionPlugin
|
from .action import ActionPlugin
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'plugin_reg', 'IntegrationPluginBase', 'ActionPlugin',
|
'ActionPlugin',
|
||||||
|
'IntegrationPluginBase',
|
||||||
|
'InvenTreePlugin',
|
||||||
|
'plugin_registry',
|
||||||
]
|
]
|
||||||
|
@ -4,43 +4,70 @@ from __future__ import unicode_literals
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
import plugin.models as models
|
import plugin.models as models
|
||||||
from plugin import plugin_reg
|
import plugin.registry as registry
|
||||||
|
|
||||||
|
|
||||||
def plugin_update(queryset, new_status: bool):
|
def plugin_update(queryset, new_status: bool):
|
||||||
"""general function for bulk changing plugins"""
|
"""
|
||||||
|
General function for bulk changing plugins
|
||||||
|
"""
|
||||||
|
|
||||||
apps_changed = False
|
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:
|
for plugin in queryset:
|
||||||
if plugin.active is not new_status:
|
if plugin.active is not new_status:
|
||||||
plugin.active = new_status
|
plugin.active = new_status
|
||||||
plugin.save(no_reload=True)
|
plugin.save(no_reload=True)
|
||||||
apps_changed = True
|
apps_changed = True
|
||||||
|
|
||||||
# reload plugins if they changed
|
# Reload plugins if they changed
|
||||||
if apps_changed:
|
if apps_changed:
|
||||||
plugin_reg.reload_plugins()
|
registry.plugin_registry.reload_plugins()
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description='Activate plugin(s)')
|
@admin.action(description='Activate plugin(s)')
|
||||||
def plugin_activate(modeladmin, request, queryset):
|
def plugin_activate(modeladmin, request, queryset):
|
||||||
"""activate a set of plugins"""
|
"""
|
||||||
|
Activate a set of plugins
|
||||||
|
"""
|
||||||
plugin_update(queryset, True)
|
plugin_update(queryset, True)
|
||||||
|
|
||||||
|
|
||||||
@admin.action(description='Deactivate plugin(s)')
|
@admin.action(description='Deactivate plugin(s)')
|
||||||
def plugin_deactivate(modeladmin, request, queryset):
|
def plugin_deactivate(modeladmin, request, queryset):
|
||||||
"""deactivate a set of plugins"""
|
"""
|
||||||
|
Deactivate a set of plugins
|
||||||
|
"""
|
||||||
|
|
||||||
plugin_update(queryset, False)
|
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):
|
class PluginConfigAdmin(admin.ModelAdmin):
|
||||||
"""Custom admin with restricted id fields"""
|
"""
|
||||||
|
Custom admin with restricted id fields
|
||||||
|
"""
|
||||||
|
|
||||||
readonly_fields = ["key", "name", ]
|
readonly_fields = ["key", "name", ]
|
||||||
list_display = ['active', '__str__', 'key', 'name', ]
|
list_display = ['name', 'key', '__str__', 'active', ]
|
||||||
list_filter = ['active']
|
list_filter = ['active']
|
||||||
actions = [plugin_activate, plugin_deactivate, ]
|
actions = [plugin_activate, plugin_deactivate, ]
|
||||||
|
inlines = [PluginSettingInline, ]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(models.PluginConfig, PluginConfigAdmin)
|
admin.site.register(models.PluginConfig, PluginConfigAdmin)
|
||||||
|
@ -11,7 +11,8 @@ from rest_framework import generics
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
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
|
import plugin.serializers as PluginSerializers
|
||||||
|
|
||||||
|
|
||||||
@ -76,7 +77,46 @@ class PluginInstall(generics.CreateAPIView):
|
|||||||
return serializer.save()
|
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_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
|
# Detail views for a single PluginConfig item
|
||||||
url(r'^(?P<pk>\d+)/', include([
|
url(r'^(?P<pk>\d+)/', include([
|
||||||
url(r'^.*$', PluginDetail.as_view(), name='api-plugin-detail'),
|
url(r'^.*$', PluginDetail.as_view(), name='api-plugin-detail'),
|
||||||
|
@ -4,17 +4,17 @@ from __future__ import unicode_literals
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from maintenance_mode.core import set_maintenance_mode
|
from maintenance_mode.core import set_maintenance_mode
|
||||||
|
|
||||||
from plugin.registry import plugins
|
from plugin import plugin_registry
|
||||||
|
|
||||||
|
|
||||||
class PluginAppConfig(AppConfig):
|
class PluginAppConfig(AppConfig):
|
||||||
name = 'plugin'
|
name = 'plugin'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
if not plugins.is_loading:
|
if not plugin_registry.is_loading:
|
||||||
# this is the first startup
|
# this is the first startup
|
||||||
plugins.collect_plugins()
|
plugin_registry.collect_plugins()
|
||||||
plugins.load_plugins()
|
plugin_registry.load_plugins()
|
||||||
|
|
||||||
# drop out of maintenance
|
# drop out of maintenance
|
||||||
# makes sure we did not have an error in reloading and maintenance is still active
|
# makes sure we did not have an error in reloading and maintenance is still active
|
||||||
|
@ -3,7 +3,9 @@ Plugin mixin classes
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
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
|
from plugin.urls import PLUGIN_BASE
|
||||||
|
|
||||||
|
|
||||||
@ -17,44 +19,38 @@ class SettingsMixin:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.add_mixin('settings', 'has_globalsettings', __class__)
|
self.add_mixin('settings', 'has_settings', __class__)
|
||||||
self.globalsettings = getattr(self, 'SETTINGS', None)
|
self.settings = getattr(self, 'SETTINGS', {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_globalsettings(self):
|
def has_settings(self):
|
||||||
"""
|
"""
|
||||||
Does this plugin use custom global settings
|
Does this plugin use custom global settings
|
||||||
"""
|
"""
|
||||||
return bool(self.globalsettings)
|
return bool(self.settings)
|
||||||
|
|
||||||
@property
|
def get_setting(self, key):
|
||||||
def globalsettingspatterns(self):
|
|
||||||
"""
|
"""
|
||||||
Get patterns for InvenTreeSetting defintion
|
Return the 'value' of the setting associated with this plugin
|
||||||
"""
|
"""
|
||||||
if self.has_globalsettings:
|
|
||||||
return {f'PLUGIN_{self.slug.upper()}_{key}': value for key, value in self.globalsettings.items()}
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _globalsetting_name(self, key):
|
return PluginSetting.get_setting(key, plugin=self)
|
||||||
"""
|
|
||||||
Get global name of setting
|
|
||||||
"""
|
|
||||||
return f'PLUGIN_{self.slug.upper()}_{key}'
|
|
||||||
|
|
||||||
def get_globalsetting(self, key):
|
def set_setting(self, key, value, user=None):
|
||||||
"""
|
"""
|
||||||
get plugin global setting by key
|
Set plugin setting value by key
|
||||||
"""
|
"""
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
return InvenTreeSetting.get_setting(self._globalsetting_name(key))
|
|
||||||
|
|
||||||
def set_globalsetting(self, key, value, user):
|
try:
|
||||||
"""
|
plugin, _ = PluginConfig.objects.get_or_create(key=self.plugin_slug(), name=self.plugin_name())
|
||||||
set plugin global setting by key
|
except (OperationalError, ProgrammingError):
|
||||||
"""
|
plugin = None
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
return InvenTreeSetting.set_setting(self._globalsetting_name(key), value, user)
|
if not plugin:
|
||||||
|
# Cannot find associated plugin model, return
|
||||||
|
return
|
||||||
|
|
||||||
|
PluginSetting.set_setting(key, value, user, plugin=plugin)
|
||||||
|
|
||||||
|
|
||||||
class UrlsMixin:
|
class UrlsMixin:
|
||||||
|
@ -10,14 +10,14 @@ from django.conf import settings
|
|||||||
|
|
||||||
# region logging / errors
|
# region logging / errors
|
||||||
def log_plugin_error(error, reference: str = 'general'):
|
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
|
# make sure the registry is set up
|
||||||
if reference not in plugin_reg.errors:
|
if reference not in plugin_registry.errors:
|
||||||
plugin_reg.errors[reference] = []
|
plugin_registry.errors[reference] = []
|
||||||
|
|
||||||
# add error to stack
|
# add error to stack
|
||||||
plugin_reg.errors[reference].append(error)
|
plugin_registry.errors[reference].append(error)
|
||||||
|
|
||||||
|
|
||||||
class IntegrationPluginError(Exception):
|
class IntegrationPluginError(Exception):
|
||||||
|
@ -9,7 +9,6 @@ import pathlib
|
|||||||
|
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.text import slugify
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
import plugin.plugin as plugin
|
import plugin.plugin as plugin
|
||||||
@ -20,19 +19,27 @@ logger = logging.getLogger("inventree")
|
|||||||
|
|
||||||
|
|
||||||
class MixinBase:
|
class MixinBase:
|
||||||
"""general base for mixins"""
|
"""
|
||||||
|
General base for mixins
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._mixinreg = {}
|
self._mixinreg = {}
|
||||||
self._mixins = {}
|
self._mixins = {}
|
||||||
|
|
||||||
def add_mixin(self, key: str, fnc_enabled=True, cls=None):
|
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._mixins[key] = fnc_enabled
|
||||||
self.setup_mixin(key, cls=cls)
|
self.setup_mixin(key, cls=cls)
|
||||||
|
|
||||||
def setup_mixin(self, key, cls=None):
|
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
|
# get human name
|
||||||
human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key
|
human_name = getattr(cls.MixinMeta, 'MIXIN_NAME', key) if cls and hasattr(cls, 'MixinMeta') else key
|
||||||
|
|
||||||
@ -44,7 +51,10 @@ class MixinBase:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def registered_mixins(self, with_base: bool = False):
|
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)
|
mixins = getattr(self, '_mixinreg', None)
|
||||||
if mixins:
|
if mixins:
|
||||||
# filter out base
|
# filter out base
|
||||||
@ -59,8 +69,6 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin):
|
|||||||
"""
|
"""
|
||||||
The IntegrationPluginBase class is used to integrate with 3rd party software
|
The IntegrationPluginBase class is used to integrate with 3rd party software
|
||||||
"""
|
"""
|
||||||
PLUGIN_SLUG = None
|
|
||||||
PLUGIN_TITLE = None
|
|
||||||
|
|
||||||
AUTHOR = None
|
AUTHOR = None
|
||||||
DESCRIPTION = None
|
DESCRIPTION = None
|
||||||
@ -84,11 +92,7 @@ class IntegrationPluginBase(MixinBase, plugin.InvenTreePlugin):
|
|||||||
# region properties
|
# region properties
|
||||||
@property
|
@property
|
||||||
def slug(self):
|
def slug(self):
|
||||||
"""slug for the plugin"""
|
return self.plugin_slug()
|
||||||
slug = getattr(self, 'PLUGIN_SLUG', None)
|
|
||||||
if not slug:
|
|
||||||
slug = self.plugin_name()
|
|
||||||
return slugify(slug)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def human_name(self):
|
def human_name(self):
|
||||||
|
@ -4,7 +4,7 @@ load templates for loaded plugins
|
|||||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from plugin import plugin_reg
|
from plugin import plugin_registry
|
||||||
|
|
||||||
|
|
||||||
class PluginTemplateLoader(FilesystemLoader):
|
class PluginTemplateLoader(FilesystemLoader):
|
||||||
@ -12,7 +12,7 @@ class PluginTemplateLoader(FilesystemLoader):
|
|||||||
def get_dirs(self):
|
def get_dirs(self):
|
||||||
dirname = 'templates'
|
dirname = 'templates'
|
||||||
template_dirs = []
|
template_dirs = []
|
||||||
for plugin in plugin_reg.plugins.values():
|
for plugin in plugin_registry.plugins.values():
|
||||||
new_path = Path(plugin.path) / dirname
|
new_path = Path(plugin.path) / dirname
|
||||||
if Path(new_path).is_dir():
|
if Path(new_path).is_dir():
|
||||||
template_dirs.append(new_path)
|
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')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -8,16 +8,17 @@ from __future__ import unicode_literals
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from plugin import plugin_reg
|
import common.models
|
||||||
|
|
||||||
|
from plugin import InvenTreePlugin, plugin_registry
|
||||||
|
|
||||||
|
|
||||||
class PluginConfig(models.Model):
|
class PluginConfig(models.Model):
|
||||||
""" A PluginConfig object holds settings for plugins.
|
"""
|
||||||
|
A PluginConfig object holds settings for plugins.
|
||||||
It is used to designate a Part as 'subscribed' for a given User.
|
|
||||||
|
|
||||||
Attributes:
|
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
|
name: PluginName of the plugin - serves for a manual double check if the right plugin is used
|
||||||
active: Should the plugin be loaded?
|
active: Should the plugin be loaded?
|
||||||
"""
|
"""
|
||||||
@ -63,12 +64,15 @@ class PluginConfig(models.Model):
|
|||||||
# functions
|
# functions
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
self.__org_active = self.active
|
self.__org_active = self.active
|
||||||
|
|
||||||
# append settings from registry
|
# 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):
|
def get_plugin_meta(name):
|
||||||
if self.plugin:
|
if self.plugin:
|
||||||
@ -82,16 +86,112 @@ class PluginConfig(models.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def save(self, force_insert=False, force_update=False, *args, **kwargs):
|
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
|
reload = kwargs.pop('no_reload', False) # check if no_reload flag is set
|
||||||
|
|
||||||
ret = super().save(force_insert, force_update, *args, **kwargs)
|
ret = super().save(force_insert, force_update, *args, **kwargs)
|
||||||
|
|
||||||
if not reload:
|
if not reload:
|
||||||
if self.active is False and self.__org_active is True:
|
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:
|
elif self.active is True and self.__org_active is False:
|
||||||
plugin_reg.reload_plugins()
|
plugin_registry.reload_plugins()
|
||||||
|
|
||||||
return ret
|
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 -*-
|
# -*- 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():
|
class InvenTreePlugin():
|
||||||
@ -7,12 +12,54 @@ class InvenTreePlugin():
|
|||||||
Base class for a plugin
|
Base class for a plugin
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
# Override the plugin name for each concrete plugin instance
|
# Override the plugin name for each concrete plugin instance
|
||||||
PLUGIN_NAME = ''
|
PLUGIN_NAME = ''
|
||||||
|
|
||||||
|
PLUGIN_SLUG = None
|
||||||
|
|
||||||
|
PLUGIN_TITLE = None
|
||||||
|
|
||||||
def plugin_name(self):
|
def plugin_name(self):
|
||||||
"""get plugin name"""
|
"""
|
||||||
|
Return the name of this plugin plugin
|
||||||
|
"""
|
||||||
return self.PLUGIN_NAME
|
return self.PLUGIN_NAME
|
||||||
|
|
||||||
def __init__(self):
|
def plugin_slug(self):
|
||||||
pass
|
|
||||||
|
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
|
Registry for loading and managing multiple plugins at run-time
|
||||||
holds the class and the object that contains all code to maintain plugin states
|
|
||||||
|
- 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 importlib
|
||||||
import pathlib
|
import pathlib
|
||||||
import logging
|
import logging
|
||||||
@ -33,7 +36,11 @@ from .helpers import get_plugin_error, IntegrationPluginError
|
|||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
class Plugins:
|
class PluginsRegistry:
|
||||||
|
"""
|
||||||
|
The PluginsRegistry class
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# plugin registry
|
# plugin registry
|
||||||
self.plugins = {}
|
self.plugins = {}
|
||||||
@ -50,15 +57,19 @@ class Plugins:
|
|||||||
# integration specific
|
# integration specific
|
||||||
self.installed_apps = [] # Holds all added plugin_paths
|
self.installed_apps = [] # Holds all added plugin_paths
|
||||||
# mixins
|
# mixins
|
||||||
self.mixins_globalsettings = {}
|
self.mixins_settings = {}
|
||||||
|
|
||||||
# region public plugin functions
|
# region public plugin functions
|
||||||
def load_plugins(self):
|
def load_plugins(self):
|
||||||
"""load and activate all IntegrationPlugins"""
|
"""
|
||||||
|
Load and activate all IntegrationPlugins
|
||||||
|
"""
|
||||||
|
|
||||||
from plugin.helpers import log_plugin_error
|
from plugin.helpers import log_plugin_error
|
||||||
|
|
||||||
logger.info('Start loading plugins')
|
logger.info('Start loading plugins')
|
||||||
# set maintanace mode
|
|
||||||
|
# Set maintanace mode
|
||||||
_maintenance = bool(get_maintenance_mode())
|
_maintenance = bool(get_maintenance_mode())
|
||||||
if not _maintenance:
|
if not _maintenance:
|
||||||
set_maintenance_mode(True)
|
set_maintenance_mode(True)
|
||||||
@ -68,7 +79,7 @@ class Plugins:
|
|||||||
retry_counter = settings.PLUGIN_RETRY
|
retry_counter = settings.PLUGIN_RETRY
|
||||||
while not registered_sucessfull:
|
while not registered_sucessfull:
|
||||||
try:
|
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._init_plugins(blocked_plugin)
|
||||||
self._activate_plugins()
|
self._activate_plugins()
|
||||||
registered_sucessfull = True
|
registered_sucessfull = True
|
||||||
@ -81,13 +92,14 @@ class Plugins:
|
|||||||
log_plugin_error({error.path: error.message}, 'load')
|
log_plugin_error({error.path: error.message}, 'load')
|
||||||
blocked_plugin = error.path # we will not try to load this app again
|
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_registry()
|
||||||
self._clean_installed_apps()
|
self._clean_installed_apps()
|
||||||
self._activate_plugins(force_reload=True)
|
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
|
retry_counter -= 1
|
||||||
|
|
||||||
if retry_counter <= 0:
|
if retry_counter <= 0:
|
||||||
if settings.PLUGIN_TESTING:
|
if settings.PLUGIN_TESTING:
|
||||||
print('[PLUGIN] Max retries, breaking loading')
|
print('[PLUGIN] Max retries, breaking loading')
|
||||||
@ -98,15 +110,20 @@ class Plugins:
|
|||||||
|
|
||||||
# now the loading will re-start up with init
|
# now the loading will re-start up with init
|
||||||
|
|
||||||
# remove maintenance
|
# Remove maintenance mode
|
||||||
if not _maintenance:
|
if not _maintenance:
|
||||||
set_maintenance_mode(False)
|
set_maintenance_mode(False)
|
||||||
|
|
||||||
logger.info('Finished loading plugins')
|
logger.info('Finished loading plugins')
|
||||||
|
|
||||||
def unload_plugins(self):
|
def unload_plugins(self):
|
||||||
"""unload and deactivate all IntegrationPlugins"""
|
"""
|
||||||
|
Unload and deactivate all IntegrationPlugins
|
||||||
|
"""
|
||||||
|
|
||||||
logger.info('Start unloading plugins')
|
logger.info('Start unloading plugins')
|
||||||
# set maintanace mode
|
|
||||||
|
# Set maintanace mode
|
||||||
_maintenance = bool(get_maintenance_mode())
|
_maintenance = bool(get_maintenance_mode())
|
||||||
if not _maintenance:
|
if not _maintenance:
|
||||||
set_maintenance_mode(True)
|
set_maintenance_mode(True)
|
||||||
@ -123,21 +140,27 @@ class Plugins:
|
|||||||
logger.info('Finished unloading plugins')
|
logger.info('Finished unloading plugins')
|
||||||
|
|
||||||
def reload_plugins(self):
|
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:
|
if self.is_loading:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info('Start reloading plugins')
|
logger.info('Start reloading plugins')
|
||||||
|
|
||||||
with maintenance_mode_on():
|
with maintenance_mode_on():
|
||||||
self.unload_plugins()
|
self.unload_plugins()
|
||||||
self.load_plugins()
|
self.load_plugins()
|
||||||
logger.info('Finished reloading plugins')
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region general plugin managment mechanisms
|
logger.info('Finished reloading plugins')
|
||||||
|
|
||||||
def collect_plugins(self):
|
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
|
self.plugin_modules = [] # clear
|
||||||
|
|
||||||
# Collect plugins from paths
|
# Collect plugins from paths
|
||||||
@ -146,7 +169,7 @@ class Plugins:
|
|||||||
if modules:
|
if modules:
|
||||||
[self.plugin_modules.append(item) for item in 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):
|
if (not settings.PLUGIN_TESTING) or (settings.PLUGIN_TESTING and settings.PLUGIN_TESTING_SETUP):
|
||||||
# Collect plugins from setup entry points
|
# Collect plugins from setup entry points
|
||||||
for entry in metadata.entry_points().get('inventree_plugins', []):
|
for entry in metadata.entry_points().get('inventree_plugins', []):
|
||||||
@ -159,22 +182,25 @@ class Plugins:
|
|||||||
logger.info(", ".join([a.__module__ for a in self.plugin_modules]))
|
logger.info(", ".join([a.__module__ for a in self.plugin_modules]))
|
||||||
|
|
||||||
def _init_plugins(self, disabled=None):
|
def _init_plugins(self, disabled=None):
|
||||||
"""initialise all found plugins
|
"""
|
||||||
|
Initialise all found plugins
|
||||||
|
|
||||||
:param disabled: loading path of disabled app, defaults to None
|
:param disabled: loading path of disabled app, defaults to None
|
||||||
:type disabled: str, optional
|
:type disabled: str, optional
|
||||||
:raises error: IntegrationPluginError
|
:raises error: IntegrationPluginError
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from plugin.models import PluginConfig
|
from plugin.models import PluginConfig
|
||||||
|
|
||||||
logger.info('Starting plugin initialisation')
|
logger.info('Starting plugin initialisation')
|
||||||
|
|
||||||
# Initialize integration plugins
|
# Initialize integration plugins
|
||||||
for plugin in self.plugin_modules:
|
for plugin in self.plugin_modules:
|
||||||
# check if package
|
# Check if package
|
||||||
was_packaged = getattr(plugin, 'is_package', False)
|
was_packaged = getattr(plugin, 'is_package', False)
|
||||||
|
|
||||||
# check if activated
|
# Check if activated
|
||||||
# these checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
|
# These checks only use attributes - never use plugin supplied functions -> that would lead to arbitrary code execution!!
|
||||||
plug_name = plugin.PLUGIN_NAME
|
plug_name = plugin.PLUGIN_NAME
|
||||||
plug_key = plugin.PLUGIN_SLUG if getattr(plugin, 'PLUGIN_SLUG', None) else plug_name
|
plug_key = plugin.PLUGIN_SLUG if getattr(plugin, 'PLUGIN_SLUG', None) else plug_name
|
||||||
plug_key = slugify(plug_key) # keys are slugs!
|
plug_key = slugify(plug_key) # keys are slugs!
|
||||||
@ -186,23 +212,23 @@ class Plugins:
|
|||||||
raise error
|
raise error
|
||||||
plugin_db_setting = None
|
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):
|
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:
|
if disabled:
|
||||||
# option1: package, option2: file-based
|
# option1: package, option2: file-based
|
||||||
if (plugin.__name__ == disabled) or (plugin.__module__ == disabled):
|
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:
|
if not settings.PLUGIN_TESTING:
|
||||||
plugin_db_setting.active = False
|
plugin_db_setting.active = False
|
||||||
# TODO save the error to the plugin
|
# TODO save the error to the plugin
|
||||||
plugin_db_setting.save(no_reload=True)
|
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
|
self.plugins_inactive[plug_key] = plugin_db_setting
|
||||||
continue # continue -> the plugin is not loaded
|
continue # continue -> the plugin is not loaded
|
||||||
|
|
||||||
# init package
|
# Initialize package
|
||||||
# now we can be sure that an admin has activated the plugin
|
# 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
|
# 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.
|
# but we could enhance those to check signatures, run the plugin against a whitelist etc.
|
||||||
@ -225,7 +251,8 @@ class Plugins:
|
|||||||
self.plugins_inactive[plug_key] = plugin_db_setting
|
self.plugins_inactive[plug_key] = plugin_db_setting
|
||||||
|
|
||||||
def _activate_plugins(self, force_reload=False):
|
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
|
:param force_reload: force reload base apps, defaults to False
|
||||||
:type force_reload: bool, optional
|
:type force_reload: bool, optional
|
||||||
@ -234,47 +261,37 @@ class Plugins:
|
|||||||
plugins = self.plugins.items()
|
plugins = self.plugins.items()
|
||||||
logger.info(f'Found {len(plugins)} active plugins')
|
logger.info(f'Found {len(plugins)} active plugins')
|
||||||
|
|
||||||
self.activate_integration_globalsettings(plugins)
|
self.activate_integration_settings(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_globalsettings()
|
self.deactivate_integration_settings()
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region specific integrations
|
def activate_integration_settings(self, plugins):
|
||||||
# region integration_globalsettings
|
|
||||||
def activate_integration_globalsettings(self, plugins):
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'):
|
if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_GLOBALSETTING'):
|
||||||
logger.info('Registering IntegrationPlugin global settings')
|
logger.info('Registering IntegrationPlugin global settings')
|
||||||
for slug, plugin in plugins:
|
for slug, plugin in plugins:
|
||||||
if plugin.mixin_enabled('settings'):
|
if plugin.mixin_enabled('settings'):
|
||||||
plugin_setting = plugin.globalsettingspatterns
|
plugin_setting = plugin.settings
|
||||||
self.mixins_globalsettings[slug] = plugin_setting
|
self.mixins_settings[slug] = plugin_setting
|
||||||
|
|
||||||
# Add to settings dir
|
def deactivate_integration_settings(self):
|
||||||
InvenTreeSetting.GLOBAL_SETTINGS.update(plugin_setting)
|
|
||||||
|
|
||||||
def deactivate_integration_globalsettings(self):
|
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
|
|
||||||
# collect all settings
|
# collect all settings
|
||||||
plugin_settings = {}
|
plugin_settings = {}
|
||||||
for _, plugin_setting in self.mixins_globalsettings.items():
|
|
||||||
|
for _, plugin_setting in self.mixins_settings.items():
|
||||||
plugin_settings.update(plugin_setting)
|
plugin_settings.update(plugin_setting)
|
||||||
|
|
||||||
# remove settings
|
|
||||||
for setting in plugin_settings:
|
|
||||||
InvenTreeSetting.GLOBAL_SETTINGS.pop(setting)
|
|
||||||
|
|
||||||
# clear cache
|
# clear cache
|
||||||
self.mixins_globalsettings = {}
|
self.mixins_Fsettings = {}
|
||||||
# endregion
|
|
||||||
|
|
||||||
# region integration_app
|
|
||||||
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
|
||||||
|
|
||||||
@ -448,8 +465,6 @@ class Plugins:
|
|||||||
return True, []
|
return True, []
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
get_plugin_error(error, do_raise=True)
|
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,
|
'default': True,
|
||||||
'validator': bool,
|
'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 = [
|
NAVIGATION = [
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
JSON serializers for Stock app
|
JSON serializers for plugin app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
@ -14,11 +14,14 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from plugin.models import PluginConfig
|
from common.serializers import SettingsSerializer
|
||||||
|
|
||||||
|
from plugin.models import PluginConfig, PluginSetting
|
||||||
|
|
||||||
|
|
||||||
class PluginConfigSerializer(serializers.ModelSerializer):
|
class PluginConfigSerializer(serializers.ModelSerializer):
|
||||||
""" Serializer for a PluginConfig:
|
"""
|
||||||
|
Serializer for a PluginConfig:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
meta = serializers.DictField(read_only=True)
|
meta = serializers.DictField(read_only=True)
|
||||||
@ -71,7 +74,7 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
|||||||
if not data.get('confirm'):
|
if not data.get('confirm'):
|
||||||
raise ValidationError({'confirm': _('Installation not confirmed')})
|
raise ValidationError({'confirm': _('Installation not confirmed')})
|
||||||
if (not data.get('url')) and (not data.get('packagename')):
|
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})
|
raise ValidationError({'url': msg, 'packagename': msg})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@ -113,7 +116,28 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
|||||||
ret['result'] = str(error.output, 'utf-8')
|
ret['result'] = str(error.output, 'utf-8')
|
||||||
ret['error'] = True
|
ret['error'] = True
|
||||||
|
|
||||||
# register plugins
|
# Register plugins
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
return ret
|
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 django.urls import reverse
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from plugin import plugin_reg
|
from plugin import plugin_registry
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
@ -16,19 +16,19 @@ register = template.Library()
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def plugin_list(*args, **kwargs):
|
def plugin_list(*args, **kwargs):
|
||||||
""" Return a list of all installed integration plugins """
|
""" Return a list of all installed integration plugins """
|
||||||
return plugin_reg.plugins
|
return plugin_registry.plugins
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inactive_plugin_list(*args, **kwargs):
|
def inactive_plugin_list(*args, **kwargs):
|
||||||
""" Return a list of all inactive integration plugins """
|
""" Return a list of all inactive integration plugins """
|
||||||
return plugin_reg.plugins_inactive
|
return plugin_registry.plugins_inactive
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def plugin_globalsettings(plugin, *args, **kwargs):
|
def plugin_settings(plugin, *args, **kwargs):
|
||||||
""" Return a list of all global settings for a plugin """
|
""" Return a list of all custom settings for a plugin """
|
||||||
return plugin_reg.mixins_globalsettings.get(plugin)
|
return plugin_registry.mixins_settings.get(plugin)
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
@ -57,4 +57,4 @@ def safe_url(view_name, *args, **kwargs):
|
|||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def plugin_errors(*args, **kwargs):
|
def plugin_errors(*args, **kwargs):
|
||||||
"""Return all plugin errors"""
|
"""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):
|
class PluginDetailAPITest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
Tests the plugin AP I endpoints
|
Tests the plugin API endpoints
|
||||||
"""
|
"""
|
||||||
|
|
||||||
roles = [
|
roles = [
|
||||||
@ -19,7 +19,7 @@ class PluginDetailAPITest(InvenTreeAPITestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
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'
|
self.PKG_NAME = 'minimal'
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@ -64,14 +64,14 @@ class PluginDetailAPITest(InvenTreeAPITestCase):
|
|||||||
Test the PluginConfig action commands
|
Test the PluginConfig action commands
|
||||||
"""
|
"""
|
||||||
from plugin.models import PluginConfig
|
from plugin.models import PluginConfig
|
||||||
from plugin import plugin_reg
|
from plugin import plugin_registry
|
||||||
|
|
||||||
url = reverse('admin:plugin_pluginconfig_changelist')
|
url = reverse('admin:plugin_pluginconfig_changelist')
|
||||||
fixtures = PluginConfig.objects.all()
|
fixtures = PluginConfig.objects.all()
|
||||||
|
|
||||||
# check if plugins were registered -> in some test setups the startup has no db access
|
# check if plugins were registered -> in some test setups the startup has no db access
|
||||||
if not fixtures:
|
if not fixtures:
|
||||||
plugin_reg.reload_plugins()
|
plugin_registry.reload_plugins()
|
||||||
fixtures = PluginConfig.objects.all()
|
fixtures = PluginConfig.objects.all()
|
||||||
|
|
||||||
print([str(a) for a in fixtures])
|
print([str(a) for a in fixtures])
|
||||||
|
@ -23,7 +23,7 @@ class BaseMixinDefinition:
|
|||||||
class SettingsMixinTest(BaseMixinDefinition, TestCase):
|
class SettingsMixinTest(BaseMixinDefinition, TestCase):
|
||||||
MIXIN_HUMAN_NAME = 'Settings'
|
MIXIN_HUMAN_NAME = 'Settings'
|
||||||
MIXIN_NAME = 'settings'
|
MIXIN_NAME = 'settings'
|
||||||
MIXIN_ENABLE_CHECK = 'has_globalsettings'
|
MIXIN_ENABLE_CHECK = 'has_settings'
|
||||||
|
|
||||||
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
|
TEST_SETTINGS = {'SETTING1': {'default': '123', }}
|
||||||
|
|
||||||
@ -42,25 +42,19 @@ class SettingsMixinTest(BaseMixinDefinition, TestCase):
|
|||||||
|
|
||||||
def test_function(self):
|
def test_function(self):
|
||||||
# settings variable
|
# settings variable
|
||||||
self.assertEqual(self.mixin.globalsettings, self.TEST_SETTINGS)
|
self.assertEqual(self.mixin.settings, 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)
|
|
||||||
|
|
||||||
# calling settings
|
# calling settings
|
||||||
# not existing
|
# not existing
|
||||||
self.assertEqual(self.mixin.get_globalsetting('ABCD'), '')
|
self.assertEqual(self.mixin.get_setting('ABCD'), '')
|
||||||
self.assertEqual(self.mixin_nothing.get_globalsetting('ABCD'), '')
|
self.assertEqual(self.mixin_nothing.get_setting('ABCD'), '')
|
||||||
|
|
||||||
# right setting
|
# right setting
|
||||||
self.mixin.set_globalsetting('SETTING1', '12345', self.test_user)
|
self.mixin.set_setting('SETTING1', '12345', self.test_user)
|
||||||
self.assertEqual(self.mixin.get_globalsetting('SETTING1'), '12345')
|
self.assertEqual(self.mixin.get_setting('SETTING1'), '12345')
|
||||||
|
|
||||||
# no setting
|
# no setting
|
||||||
self.assertEqual(self.mixin_nothing.get_globalsetting(''), '')
|
self.assertEqual(self.mixin_nothing.get_setting(''), '')
|
||||||
|
|
||||||
|
|
||||||
class UrlsMixinTest(BaseMixinDefinition, TestCase):
|
class UrlsMixinTest(BaseMixinDefinition, TestCase):
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
""" Unit tests for plugins """
|
"""
|
||||||
|
Unit tests for plugins
|
||||||
|
"""
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
@ -6,9 +8,8 @@ import plugin.plugin
|
|||||||
import plugin.integration
|
import plugin.integration
|
||||||
from plugin.samples.integration.sample import SampleIntegrationPlugin
|
from plugin.samples.integration.sample import SampleIntegrationPlugin
|
||||||
from plugin.samples.integration.another_sample import WrongIntegrationPlugin, NoIntegrationPlugin
|
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
|
import plugin.templatetags.plugin_extras as plugin_tags
|
||||||
from plugin import plugin_reg
|
from plugin import plugin_registry
|
||||||
|
|
||||||
|
|
||||||
class InvenTreePluginTests(TestCase):
|
class InvenTreePluginTests(TestCase):
|
||||||
@ -57,17 +58,17 @@ class PluginTagTests(TestCase):
|
|||||||
|
|
||||||
def test_tag_plugin_list(self):
|
def test_tag_plugin_list(self):
|
||||||
"""test that all plugins are listed"""
|
"""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):
|
def test_tag_incative_plugin_list(self):
|
||||||
"""test that all inactive plugins are listed"""
|
"""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"""
|
"""check all plugins are listed"""
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
plugin_tags.plugin_globalsettings(self.sample),
|
plugin_tags.plugin_settings(self.sample),
|
||||||
plugin_reg.mixins_globalsettings.get(self.sample)
|
plugin_registry.mixins_settings.get(self.sample)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tag_mixin_enabled(self):
|
def test_tag_mixin_enabled(self):
|
||||||
@ -89,4 +90,4 @@ class PluginTagTests(TestCase):
|
|||||||
|
|
||||||
def test_tag_plugin_errors(self):
|
def test_tag_plugin_errors(self):
|
||||||
"""test that all errors are listed"""
|
"""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
|
URL lookup for plugin app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
from plugin import plugin_reg
|
from plugin import plugin_registry
|
||||||
|
|
||||||
|
|
||||||
PLUGIN_BASE = 'plugin' # Constant for links
|
PLUGIN_BASE = 'plugin' # Constant for links
|
||||||
|
|
||||||
|
|
||||||
def get_plugin_urls():
|
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 = []
|
urls = []
|
||||||
for plugin in plugin_reg.plugins.values():
|
|
||||||
|
for plugin in plugin_registry.plugins.values():
|
||||||
if plugin.mixin_enabled('urls'):
|
if plugin.mixin_enabled('urls'):
|
||||||
urls.append(plugin.urlpatterns)
|
urls.append(plugin.urlpatterns)
|
||||||
|
|
||||||
return url(f'^{PLUGIN_BASE}/', include((urls, 'plugin')))
|
return url(f'^{PLUGIN_BASE}/', include((urls, 'plugin')))
|
||||||
|
@ -13,19 +13,19 @@
|
|||||||
|
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_SSO" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_SSO" icon="fa-user-shield" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-user-lock" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-at" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENFORCE_MFA" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENFORCE_MFA" icon='fa-key' %}
|
||||||
<tr>
|
<tr>
|
||||||
<th><h5>{% trans 'Signup' %}</h5></th>
|
<th><h5>{% trans 'Signup' %}</h5></th>
|
||||||
<td colspan='4'></td>
|
<td colspan='4'></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-user-plus" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_MAIL_TWICE" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_MAIL_TWICE" icon="fa-at" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_PWD_TWICE" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_PWD_TWICE" icon="fa-user-lock" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_SSO_AUTO" icon="fa-info-circle" %}
|
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_SSO_AUTO" icon="fa-key" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="SIGNUP_GROUP" %}
|
{% include "InvenTree/settings/setting.html" with key="SIGNUP_GROUP" icon="fa-users" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -5,12 +5,12 @@
|
|||||||
<h4>{% trans "Settings" %}</h4>
|
<h4>{% trans "Settings" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% plugin_globalsettings plugin_key as plugin_settings %}
|
{% plugin_settings plugin_key as plugin_settings %}
|
||||||
|
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for setting in plugin_settings %}
|
{% for setting in plugin_settings %}
|
||||||
{% include "InvenTree/settings/setting.html" with key=setting%}
|
{% include "InvenTree/settings/setting.html" with key=setting plugin=plugin %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
@ -19,10 +19,9 @@
|
|||||||
<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_URL" %}
|
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_URL" icon="fa-link" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_NAVIGATION" %}
|
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_NAVIGATION" icon="fa-sitemap" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_GLOBALSETTING"%}
|
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP" icon="fa-rocket" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="ENABLE_PLUGINS_APP"%}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -70,7 +69,7 @@
|
|||||||
{% if mixin_list %}
|
{% if mixin_list %}
|
||||||
{% for mixin in mixin_list %}
|
{% for mixin in mixin_list %}
|
||||||
<a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar">
|
<a class='sidebar-selector' id='select-plugin-{{plugin_key}}' data-bs-parent="#sidebar">
|
||||||
<span class='badge bg-dark badge-right'>{{ mixin.human_name }}</span>
|
<span class='badge bg-dark badge-right rounded-pill'>{{ mixin.human_name }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -12,10 +12,10 @@
|
|||||||
|
|
||||||
<table class='table table-striped table-condensed'>
|
<table class='table table-striped table-condensed'>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE" %}
|
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE" icon="file-pdf" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" %}
|
{% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" icon="fa-print" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" %}
|
{% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" icon="fa-laptop-code" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" %}
|
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" icon="fa-vial" %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% if user_setting %}
|
{% if plugin %}
|
||||||
{% setting_object key user=request.user as setting %}
|
{% setting_object key plugin=plugin as setting %}
|
||||||
|
{% elif user_setting %}
|
||||||
|
{% setting_object key user=request.user as setting %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% setting_object key as setting %}
|
{% setting_object key as setting %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
@ -13,7 +15,7 @@
|
|||||||
<span class='fas {{ icon }}'></span>
|
<span class='fas {{ icon }}'></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><strong>{% trans setting.name %}</strong></td>
|
<td><strong>{{ setting.name }}</strong></td>
|
||||||
<td>
|
<td>
|
||||||
{% if setting.is_bool %}
|
{% if setting.is_bool %}
|
||||||
<div class='form-check form-switch'>
|
<div class='form-check form-switch'>
|
||||||
@ -32,11 +34,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{% trans setting.description %}
|
{{ setting.description }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class='btn-group float-right'>
|
<div class='btn-group float-right'>
|
||||||
<button class='btn btn-outline-secondary btn-small btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if user_setting %}user='{{request.user.id}}'{% endif %}>
|
<button class='btn btn-outline-secondary btn-small btn-edit-setting' pk='{{ setting.pk }}' setting='{{ setting.key.upper }}' title='{% trans "Edit setting" %}' {% if plugin %}plugin='{{ plugin.pk }}'{% endif %}{% if user_setting %}user='{{request.user.id}}'{% endif %}>
|
||||||
<span class='fas fa-edit icon-green'></span>
|
<span class='fas fa-edit icon-green'></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,16 +62,27 @@
|
|||||||
$('table').find('.btn-edit-setting').click(function() {
|
$('table').find('.btn-edit-setting').click(function() {
|
||||||
var setting = $(this).attr('setting');
|
var setting = $(this).attr('setting');
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
var plugin = $(this).attr('plugin');
|
||||||
var is_global = true;
|
var is_global = true;
|
||||||
|
|
||||||
if ($(this).attr('user')){
|
if ($(this).attr('user')){
|
||||||
is_global = false;
|
is_global = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var title = '';
|
||||||
|
|
||||||
|
if (plugin != null) {
|
||||||
|
title = '{% trans "Edit Plugin Setting" %}';
|
||||||
|
} else if (is_global) {
|
||||||
|
title = '{% trans "Edit Global Setting" %}';
|
||||||
|
} else {
|
||||||
|
title = '{% trans "Edit User Setting" %}';
|
||||||
|
}
|
||||||
|
|
||||||
editSetting(pk, {
|
editSetting(pk, {
|
||||||
|
plugin: plugin,
|
||||||
global: is_global,
|
global: is_global,
|
||||||
title: is_global ? '{% trans "Edit Global Setting" %}' : '{% trans "Edit User Setting" %}',
|
title: title,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
{% include "sidebar_header.html" with text="Plugin Settings" %}
|
{% include "sidebar_header.html" with text="Plugin Settings" %}
|
||||||
|
|
||||||
{% include "sidebar_item.html" with label='plugin' text="Plugin" icon="fa-plug" %}
|
{% include "sidebar_item.html" with label='plugin' text="Plugins" icon="fa-plug" %}
|
||||||
|
|
||||||
{% plugin_list as pl_list %}
|
{% plugin_list as pl_list %}
|
||||||
{% for plugin_key, plugin in pl_list.items %}
|
{% for plugin_key, plugin in pl_list.items %}
|
||||||
|
@ -28,9 +28,13 @@ function editSetting(pk, options={}) {
|
|||||||
// Is this a global setting or a user setting?
|
// Is this a global setting or a user setting?
|
||||||
var global = options.global || false;
|
var global = options.global || false;
|
||||||
|
|
||||||
|
var plugin = options.plugin;
|
||||||
|
|
||||||
var url = '';
|
var url = '';
|
||||||
|
|
||||||
if (global) {
|
if (plugin) {
|
||||||
|
url = `/api/plugin/settings/${pk}/`;
|
||||||
|
} else if (global) {
|
||||||
url = `/api/settings/global/${pk}/`;
|
url = `/api/settings/global/${pk}/`;
|
||||||
} else {
|
} else {
|
||||||
url = `/api/settings/user/${pk}/`;
|
url = `/api/settings/user/${pk}/`;
|
||||||
|
@ -76,7 +76,8 @@ class RuleSet(models.Model):
|
|||||||
'otp_totp_totpdevice',
|
'otp_totp_totpdevice',
|
||||||
'otp_static_statictoken',
|
'otp_static_statictoken',
|
||||||
'otp_static_staticdevice',
|
'otp_static_staticdevice',
|
||||||
'plugin_pluginconfig'
|
'plugin_pluginconfig',
|
||||||
|
'plugin_pluginsetting',
|
||||||
],
|
],
|
||||||
'part_category': [
|
'part_category': [
|
||||||
'part_partcategory',
|
'part_partcategory',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user