Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added required attribute to settings/plugins, refactor: allValues #5224

Merged
merged 4 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 76 additions & 22 deletions InvenTree/common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"""

Expand All @@ -140,6 +141,7 @@ class SettingsKeyType(TypedDict, total=False):
before_save: Callable[..., None]
after_save: Callable[..., None]
protected: bool
required: bool
model: str


Expand Down Expand Up @@ -250,59 +252,99 @@ def get_filters_for_instance(self):
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:
# Keys which start with an underscore are used for internal functionality
results = results.exclude(key__startswith='_')

# Optionally filter by other keys
results = results.filter(**cls.get_filters(**kwargs))
results = results.filter(**filters)

# Query the database
settings = {}
settings: Dict[str, BaseInvenTreeSetting] = {}

# Query the database
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)

if exclude_hidden:
hidden = cls.SETTINGS[key].get('hidden', False)
settings[key.upper()] = cls(
key=key.upper(),
value=cls.get_setting_default(key, **filters),
**filters
)

if hidden:
# Remove hidden items
del settings[key.upper()]
# remove any hidden settings
if exclude_hidden and setting.get("hidden", False):
del settings[key.upper()]

for key, value in settings.items():
# format settings values and remove protected
for key, setting in settings.items():
validator = cls.get_setting_validator(key)

if cls.is_protected(key):
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)
setting.value = cls.get_setting_default(key, **filters)

settings[key] = value
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)

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.
Expand Down Expand Up @@ -829,7 +871,7 @@ def as_int(self):
@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)

Expand All @@ -838,6 +880,18 @@ def protected(self):
"""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."""
Expand Down
13 changes: 13 additions & 0 deletions InvenTree/plugin/base/integration/SettingsMixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,16 @@ def set_setting(self, key, value, user=None):
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())
4 changes: 3 additions & 1 deletion docs/docs/extend/plugins/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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.