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:
Lukas 2023-07-12 00:19:19 +02:00 committed by GitHub
parent b3dcc28bd9
commit ee274739a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 142 additions and 26 deletions

View File

@ -127,6 +127,7 @@ class SettingsKeyType(TypedDict, total=False):
before_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)
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)
"""
@ -140,6 +141,7 @@ class SettingsKeyType(TypedDict, total=False):
before_save: Callable[..., None]
after_save: Callable[..., None]
protected: bool
required: bool
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)}
@classmethod
def allValues(cls, exclude_hidden=False, **kwargs):
"""Return a dict of "all" defined global settings.
def all_settings(cls, *, exclude_hidden=False, settings_definition: Union[Dict[str, SettingsKeyType], None] = None, **kwargs):
"""Return a list of "all" defined settings.
This performs a single database lookup,
and then any settings which are not *in* the database
are assigned their default values
"""
filters = cls.get_filters(**kwargs)
results = cls.objects.all()
if exclude_hidden:
@ -264,45 +268,83 @@ class BaseInvenTreeSetting(models.Model):
results = results.exclude(key__startswith='_')
# Optionally filter by other keys
results = results.filter(**cls.get_filters(**kwargs))
results = results.filter(**filters)
settings: Dict[str, BaseInvenTreeSetting] = {}
# Query the database
settings = {}
for setting in results:
if setting.key:
settings[setting.key.upper()] = setting.value
settings[setting.key.upper()] = setting
# 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:
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:
hidden = cls.SETTINGS[key].get('hidden', False)
# remove any hidden settings
if exclude_hidden and setting.get("hidden", False):
del settings[key.upper()]
if hidden:
# Remove hidden items
del settings[key.upper()]
# format settings values and remove protected
for key, setting in settings.items():
validator = cls.get_setting_validator(key, **filters)
for key, value in settings.items():
validator = cls.get_setting_validator(key)
if cls.is_protected(key):
value = '***'
if cls.is_protected(key, **filters) and setting.value != "":
setting.value = '***'
elif cls.validator_is_bool(validator):
value = InvenTree.helpers.str2bool(value)
setting.value = InvenTree.helpers.str2bool(setting.value)
elif cls.validator_is_int(validator):
try:
value = int(value)
setting.value = int(setting.value)
except ValueError:
value = cls.get_setting_default(key)
settings[key] = value
setting.value = cls.get_setting_default(key, **filters)
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
def get_setting_definition(cls, key, **kwargs):
"""Return the 'definition' of a particular settings value, as a dict object.
@ -829,7 +871,7 @@ class BaseInvenTreeSetting(models.Model):
@classmethod
def is_protected(cls, key, **kwargs):
"""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)
@ -838,6 +880,18 @@ class BaseInvenTreeSetting(models.Model):
"""Returns if setting is protected from rendering."""
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():
"""Build up group tuple for settings based on your choices."""

View File

@ -5,6 +5,7 @@ import json
import time
from datetime import timedelta
from http import HTTPStatus
from unittest import mock
from django.contrib.auth import get_user_model
from django.core.cache import cache
@ -105,6 +106,40 @@ class SettingsTest(InvenTreeTestCase):
self.assertIn('PART_COPY_TESTS', result)
self.assertIn('STOCK_OWNERSHIP_CONTROL', 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):
"""Test that all settings are valid.

View File

@ -84,3 +84,16 @@ class SettingsMixin:
return
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())

View File

@ -44,6 +44,7 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi
'API_KEY': {
'name': _('API Key'),
'description': _('Key required for accessing external API'),
'required': True,
},
'NUMERICAL_SETTING': {
'name': _('Numerical'),

View File

@ -1,6 +1,7 @@
"""Unit tests for action plugins."""
from InvenTree.unit_test import InvenTreeTestCase
from plugin import registry
class SampleIntegrationPluginTests(InvenTreeTestCase):
@ -11,3 +12,13 @@ class SampleIntegrationPluginTests(InvenTreeTestCase):
response = self.client.get('/plugin/sample/ho/he/')
self.assertEqual(response.status_code, 200)
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, []))

View File

@ -35,6 +35,7 @@ class PluginWithSettings(SettingsMixin, InvenTreePlugin):
'name': 'API Key',
'description': 'Security key for accessing remote API',
'default': '',
'required': True,
},
'API_URL': {
'name': _('API URL'),
@ -71,10 +72,11 @@ class PluginWithSettings(SettingsMixin, InvenTreePlugin):
!!! tip "Hidden Settings"
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
api_url = self.get_setting('API_URL', cache = False)
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.