mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 21:38:48 +00:00
Added required attribute to settings/plugins, refactor: allValues (#5224)
* Added required attribute to settings/plugins, refactor: allValues - added 'required' attribute to InvenTreeBaseSetting - added 'check_all_settings' - added 'all_settings' to get a list of all defined settings - refactored 'allValues' to use new 'all_settings' function - added docs for new 'check_setting' function on plugin SettingsMixin * Fix typing to be compatible with python 3.9 * trigger: ci * Fixed **kwargs bug and added tests
This commit is contained in:
parent
b3dcc28bd9
commit
ee274739a6
@ -127,6 +127,7 @@ class SettingsKeyType(TypedDict, total=False):
|
|||||||
before_save: Function that gets called after save with *args, **kwargs (optional)
|
before_save: Function that gets called after save with *args, **kwargs (optional)
|
||||||
after_save: Function that gets called after save with *args, **kwargs (optional)
|
after_save: Function that gets called after save with *args, **kwargs (optional)
|
||||||
protected: Protected values are not returned to the client, instead "***" is returned (optional, default: False)
|
protected: Protected values are not returned to the client, instead "***" is returned (optional, default: False)
|
||||||
|
required: Is this setting required to work, can be used in combination with .check_all_settings(...) (optional, default: False)
|
||||||
model: Auto create a dropdown menu to select an associated model instance (e.g. 'company.company', 'auth.user' and 'auth.group' are possible too, optional)
|
model: Auto create a dropdown menu to select an associated model instance (e.g. 'company.company', 'auth.user' and 'auth.group' are possible too, optional)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -140,6 +141,7 @@ class SettingsKeyType(TypedDict, total=False):
|
|||||||
before_save: Callable[..., None]
|
before_save: Callable[..., None]
|
||||||
after_save: Callable[..., None]
|
after_save: Callable[..., None]
|
||||||
protected: bool
|
protected: bool
|
||||||
|
required: bool
|
||||||
model: str
|
model: str
|
||||||
|
|
||||||
|
|
||||||
@ -250,13 +252,15 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
return {key: getattr(self, key, None) for key in self.extra_unique_fields if hasattr(self, key)}
|
return {key: getattr(self, key, None) for key in self.extra_unique_fields if hasattr(self, key)}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def allValues(cls, exclude_hidden=False, **kwargs):
|
def all_settings(cls, *, exclude_hidden=False, settings_definition: Union[Dict[str, SettingsKeyType], None] = None, **kwargs):
|
||||||
"""Return a dict of "all" defined global settings.
|
"""Return a list of "all" defined settings.
|
||||||
|
|
||||||
This performs a single database lookup,
|
This performs a single database lookup,
|
||||||
and then any settings which are not *in* the database
|
and then any settings which are not *in* the database
|
||||||
are assigned their default values
|
are assigned their default values
|
||||||
"""
|
"""
|
||||||
|
filters = cls.get_filters(**kwargs)
|
||||||
|
|
||||||
results = cls.objects.all()
|
results = cls.objects.all()
|
||||||
|
|
||||||
if exclude_hidden:
|
if exclude_hidden:
|
||||||
@ -264,45 +268,83 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
results = results.exclude(key__startswith='_')
|
results = results.exclude(key__startswith='_')
|
||||||
|
|
||||||
# Optionally filter by other keys
|
# Optionally filter by other keys
|
||||||
results = results.filter(**cls.get_filters(**kwargs))
|
results = results.filter(**filters)
|
||||||
|
|
||||||
|
settings: Dict[str, BaseInvenTreeSetting] = {}
|
||||||
|
|
||||||
# Query the database
|
# Query the database
|
||||||
settings = {}
|
|
||||||
|
|
||||||
for setting in results:
|
for setting in results:
|
||||||
if setting.key:
|
if setting.key:
|
||||||
settings[setting.key.upper()] = setting.value
|
settings[setting.key.upper()] = setting
|
||||||
|
|
||||||
# Specify any "default" values which are not in the database
|
# Specify any "default" values which are not in the database
|
||||||
for key in cls.SETTINGS.keys():
|
settings_definition = settings_definition or cls.SETTINGS
|
||||||
|
for key, setting in settings_definition.items():
|
||||||
if key.upper() not in settings:
|
if key.upper() not in settings:
|
||||||
settings[key.upper()] = cls.get_setting_default(key)
|
settings[key.upper()] = cls(
|
||||||
|
key=key.upper(),
|
||||||
|
value=cls.get_setting_default(key, **filters),
|
||||||
|
**filters
|
||||||
|
)
|
||||||
|
|
||||||
if exclude_hidden:
|
# remove any hidden settings
|
||||||
hidden = cls.SETTINGS[key].get('hidden', False)
|
if exclude_hidden and setting.get("hidden", False):
|
||||||
|
del settings[key.upper()]
|
||||||
|
|
||||||
if hidden:
|
# format settings values and remove protected
|
||||||
# Remove hidden items
|
for key, setting in settings.items():
|
||||||
del settings[key.upper()]
|
validator = cls.get_setting_validator(key, **filters)
|
||||||
|
|
||||||
for key, value in settings.items():
|
if cls.is_protected(key, **filters) and setting.value != "":
|
||||||
validator = cls.get_setting_validator(key)
|
setting.value = '***'
|
||||||
|
|
||||||
if cls.is_protected(key):
|
|
||||||
value = '***'
|
|
||||||
elif cls.validator_is_bool(validator):
|
elif cls.validator_is_bool(validator):
|
||||||
value = InvenTree.helpers.str2bool(value)
|
setting.value = InvenTree.helpers.str2bool(setting.value)
|
||||||
elif cls.validator_is_int(validator):
|
elif cls.validator_is_int(validator):
|
||||||
try:
|
try:
|
||||||
value = int(value)
|
setting.value = int(setting.value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
value = cls.get_setting_default(key)
|
setting.value = cls.get_setting_default(key, **filters)
|
||||||
|
|
||||||
settings[key] = value
|
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def allValues(cls, *, exclude_hidden=False, settings_definition: Union[Dict[str, SettingsKeyType], None] = None, **kwargs):
|
||||||
|
"""Return a dict of "all" defined global settings.
|
||||||
|
|
||||||
|
This performs a single database lookup,
|
||||||
|
and then any settings which are not *in* the database
|
||||||
|
are assigned their default values
|
||||||
|
"""
|
||||||
|
all_settings = cls.all_settings(exclude_hidden=exclude_hidden, settings_definition=settings_definition, **kwargs)
|
||||||
|
|
||||||
|
settings: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
for key, setting in all_settings.items():
|
||||||
|
settings[key] = setting.value
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_all_settings(cls, *, exclude_hidden=False, settings_definition: Union[Dict[str, SettingsKeyType], None] = None, **kwargs):
|
||||||
|
"""Check if all required settings are set by definition.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
is_valid: Are all required settings defined
|
||||||
|
missing_settings: List of all settings that are missing (empty if is_valid is 'True')
|
||||||
|
"""
|
||||||
|
all_settings = cls.all_settings(exclude_hidden=exclude_hidden, settings_definition=settings_definition, **kwargs)
|
||||||
|
|
||||||
|
missing_settings: List[str] = []
|
||||||
|
|
||||||
|
for setting in all_settings.values():
|
||||||
|
if setting.required:
|
||||||
|
value = setting.value or cls.get_setting_default(setting.key, **kwargs)
|
||||||
|
|
||||||
|
if value == "":
|
||||||
|
missing_settings.append(setting.key.upper())
|
||||||
|
|
||||||
|
return len(missing_settings) == 0, missing_settings
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_definition(cls, key, **kwargs):
|
def get_setting_definition(cls, key, **kwargs):
|
||||||
"""Return the 'definition' of a particular settings value, as a dict object.
|
"""Return the 'definition' of a particular settings value, as a dict object.
|
||||||
@ -829,7 +871,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def is_protected(cls, key, **kwargs):
|
def is_protected(cls, key, **kwargs):
|
||||||
"""Check if the setting value is protected."""
|
"""Check if the setting value is protected."""
|
||||||
setting = cls.get_setting_definition(key, **kwargs)
|
setting = cls.get_setting_definition(key, **cls.get_filters(**kwargs))
|
||||||
|
|
||||||
return setting.get('protected', False)
|
return setting.get('protected', False)
|
||||||
|
|
||||||
@ -838,6 +880,18 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
"""Returns if setting is protected from rendering."""
|
"""Returns if setting is protected from rendering."""
|
||||||
return self.__class__.is_protected(self.key, **self.get_filters_for_instance())
|
return self.__class__.is_protected(self.key, **self.get_filters_for_instance())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_required(cls, key, **kwargs):
|
||||||
|
"""Check if this setting value is required."""
|
||||||
|
setting = cls.get_setting_definition(key, **cls.get_filters(**kwargs))
|
||||||
|
|
||||||
|
return setting.get("required", False)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required(self):
|
||||||
|
"""Returns if setting is required."""
|
||||||
|
return self.__class__.is_required(self.key, **self.get_filters_for_instance())
|
||||||
|
|
||||||
|
|
||||||
def settings_group_options():
|
def settings_group_options():
|
||||||
"""Build up group tuple for settings based on your choices."""
|
"""Build up group tuple for settings based on your choices."""
|
||||||
|
@ -5,6 +5,7 @@ import json
|
|||||||
import time
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@ -105,6 +106,40 @@ class SettingsTest(InvenTreeTestCase):
|
|||||||
self.assertIn('PART_COPY_TESTS', result)
|
self.assertIn('PART_COPY_TESTS', result)
|
||||||
self.assertIn('STOCK_OWNERSHIP_CONTROL', result)
|
self.assertIn('STOCK_OWNERSHIP_CONTROL', result)
|
||||||
self.assertIn('SIGNUP_GROUP', result)
|
self.assertIn('SIGNUP_GROUP', result)
|
||||||
|
self.assertIn('SERVER_RESTART_REQUIRED', result)
|
||||||
|
|
||||||
|
result = InvenTreeSetting.allValues(exclude_hidden=True)
|
||||||
|
self.assertNotIn('SERVER_RESTART_REQUIRED', result)
|
||||||
|
|
||||||
|
def test_all_settings(self):
|
||||||
|
"""Make sure that the all_settings function returns correctly"""
|
||||||
|
result = InvenTreeSetting.all_settings()
|
||||||
|
self.assertIn("INVENTREE_INSTANCE", result)
|
||||||
|
self.assertIsInstance(result['INVENTREE_INSTANCE'], InvenTreeSetting)
|
||||||
|
|
||||||
|
@mock.patch("common.models.InvenTreeSetting.get_setting_definition")
|
||||||
|
def test_check_all_settings(self, get_setting_definition):
|
||||||
|
"""Make sure that the check_all_settings function returns correctly"""
|
||||||
|
# define partial schema
|
||||||
|
settings_definition = {
|
||||||
|
"AB": { # key that's has not already been accessed
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
|
"CD": {
|
||||||
|
"required": True,
|
||||||
|
"protected": True,
|
||||||
|
},
|
||||||
|
"EF": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
def mocked(key, **kwargs):
|
||||||
|
return settings_definition.get(key, {})
|
||||||
|
get_setting_definition.side_effect = mocked
|
||||||
|
|
||||||
|
self.assertEqual(InvenTreeSetting.check_all_settings(settings_definition=settings_definition), (False, ["AB", "CD"]))
|
||||||
|
InvenTreeSetting.set_setting('AB', "hello", self.user)
|
||||||
|
InvenTreeSetting.set_setting('CD', "world", self.user)
|
||||||
|
self.assertEqual(InvenTreeSetting.check_all_settings(), (True, []))
|
||||||
|
|
||||||
def run_settings_check(self, key, setting):
|
def run_settings_check(self, key, setting):
|
||||||
"""Test that all settings are valid.
|
"""Test that all settings are valid.
|
||||||
|
@ -84,3 +84,16 @@ class SettingsMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
PluginSetting.set_setting(key, value, user, plugin=plugin)
|
PluginSetting.set_setting(key, value, user, plugin=plugin)
|
||||||
|
|
||||||
|
def check_settings(self):
|
||||||
|
"""Check if all required settings for this machine are defined.
|
||||||
|
|
||||||
|
Warning: This method cannot be used in the __init__ function of the plugin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
is_valid: Are all required settings defined
|
||||||
|
missing_settings: List of all settings that are missing (empty if is_valid is 'True')
|
||||||
|
"""
|
||||||
|
from plugin.models import PluginSetting
|
||||||
|
|
||||||
|
return PluginSetting.check_all_settings(settings_definition=self.settings, plugin=self.plugin_config())
|
||||||
|
@ -44,6 +44,7 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi
|
|||||||
'API_KEY': {
|
'API_KEY': {
|
||||||
'name': _('API Key'),
|
'name': _('API Key'),
|
||||||
'description': _('Key required for accessing external API'),
|
'description': _('Key required for accessing external API'),
|
||||||
|
'required': True,
|
||||||
},
|
},
|
||||||
'NUMERICAL_SETTING': {
|
'NUMERICAL_SETTING': {
|
||||||
'name': _('Numerical'),
|
'name': _('Numerical'),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Unit tests for action plugins."""
|
"""Unit tests for action plugins."""
|
||||||
|
|
||||||
from InvenTree.unit_test import InvenTreeTestCase
|
from InvenTree.unit_test import InvenTreeTestCase
|
||||||
|
from plugin import registry
|
||||||
|
|
||||||
|
|
||||||
class SampleIntegrationPluginTests(InvenTreeTestCase):
|
class SampleIntegrationPluginTests(InvenTreeTestCase):
|
||||||
@ -11,3 +12,13 @@ class SampleIntegrationPluginTests(InvenTreeTestCase):
|
|||||||
response = self.client.get('/plugin/sample/ho/he/')
|
response = self.client.get('/plugin/sample/ho/he/')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.content, b'Hi there testuser this works')
|
self.assertEqual(response.content, b'Hi there testuser this works')
|
||||||
|
|
||||||
|
def test_settings(self):
|
||||||
|
"""Check the SettingsMixin.check_settings function."""
|
||||||
|
plugin = registry.get_plugin('sample')
|
||||||
|
self.assertIsNotNone(plugin)
|
||||||
|
|
||||||
|
# check settings
|
||||||
|
self.assertEqual(plugin.check_settings(), (False, ['API_KEY']))
|
||||||
|
plugin.set_setting('API_KEY', "dsfiodsfjsfdjsf")
|
||||||
|
self.assertEqual(plugin.check_settings(), (True, []))
|
||||||
|
@ -35,6 +35,7 @@ class PluginWithSettings(SettingsMixin, InvenTreePlugin):
|
|||||||
'name': 'API Key',
|
'name': 'API Key',
|
||||||
'description': 'Security key for accessing remote API',
|
'description': 'Security key for accessing remote API',
|
||||||
'default': '',
|
'default': '',
|
||||||
|
'required': True,
|
||||||
},
|
},
|
||||||
'API_URL': {
|
'API_URL': {
|
||||||
'name': _('API URL'),
|
'name': _('API URL'),
|
||||||
@ -71,10 +72,11 @@ class PluginWithSettings(SettingsMixin, InvenTreePlugin):
|
|||||||
!!! tip "Hidden Settings"
|
!!! tip "Hidden Settings"
|
||||||
Plugin settings can be hidden from the settings page by marking them as 'hidden'
|
Plugin settings can be hidden from the settings page by marking them as 'hidden'
|
||||||
|
|
||||||
This mixin defines the helper functions `plugin.get_setting` and `plugin.set_setting` to access all plugin specific settings:
|
This mixin defines the helper functions `plugin.get_setting`, `plugin.set_setting` and `plugin.check_settings` to access all plugin specific settings. The `plugin.check_settings` function can be used to check if all settings marked with `'required': True` are defined and not equal to `''`. Note that these methods cannot be used in the `__init__` function of your plugin.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
api_url = self.get_setting('API_URL', cache = False)
|
api_url = self.get_setting('API_URL', cache = False)
|
||||||
self.set_setting('API_URL', 'some value')
|
self.set_setting('API_URL', 'some value')
|
||||||
|
is_valid, missing_settings = self.check_settings()
|
||||||
```
|
```
|
||||||
`get_setting` has an additional parameter which lets control if the value is taken directly from the database or from the cache. If it is left away `False` is the default that means the value is taken directly from the database.
|
`get_setting` has an additional parameter which lets control if the value is taken directly from the database or from the cache. If it is left away `False` is the default that means the value is taken directly from the database.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user