mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Refactor: BaseInvenTreeSetting (#4834)
* Added typing for settings * Refactored common.BaseInvenTreeSetting model to make it more generic * Use older syntax for union types * Added protected option to typing * Remove now unused code * Remove old 'get_kwargs' method as it is replaced by 'get_filters_for_instance' * Trigger ci
This commit is contained in:
parent
61481b4eb0
commit
cb8ae10280
@ -16,6 +16,7 @@ import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from secrets import compare_digest
|
||||
from typing import Any, Callable, Dict, List, Tuple, TypedDict, Union
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
@ -112,10 +113,45 @@ class ProjectCode(InvenTree.models.MetadataMixin, models.Model):
|
||||
)
|
||||
|
||||
|
||||
class BaseInvenTreeSetting(models.Model):
|
||||
"""An base InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values)."""
|
||||
class SettingsKeyType(TypedDict, total=False):
|
||||
"""Type definitions for a SettingsKeyType
|
||||
|
||||
SETTINGS = {}
|
||||
Attributes:
|
||||
name: Translatable string name of the setting (required)
|
||||
description: Translatable string description of the setting (required)
|
||||
units: Units of the particular setting (optional)
|
||||
validator: Validation function/list of functions for the setting (optional, default: None, e.g: bool, int, str, MinValueValidator, ...)
|
||||
default: Default value or function that returns default value (optional)
|
||||
choices: (Function that returns) Tuple[str: key, str: display value] (optional)
|
||||
hidden: Hide this setting from settings page (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)
|
||||
protected: Protected values are not returned to the client, instead "***" is returned (optional, default: False)
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
units: str
|
||||
validator: Union[Callable, List[Callable], Tuple[Callable]]
|
||||
default: Union[Callable, Any]
|
||||
choices: Union[Tuple[str, str], Callable[[], Tuple[str, str]]]
|
||||
hidden: bool
|
||||
before_save: Callable[..., None]
|
||||
after_save: Callable[..., None]
|
||||
protected: bool
|
||||
|
||||
|
||||
class BaseInvenTreeSetting(models.Model):
|
||||
"""An base InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values).
|
||||
|
||||
Attributes:
|
||||
SETTINGS: definition of all available settings
|
||||
extra_unique_fields: List of extra fields used to be unique, e.g. for PluginConfig -> plugin
|
||||
"""
|
||||
|
||||
SETTINGS: Dict[str, SettingsKeyType] = {}
|
||||
|
||||
extra_unique_fields: List[str] = []
|
||||
|
||||
class Meta:
|
||||
"""Meta options for BaseInvenTreeSetting -> abstract stops creation of database entry."""
|
||||
@ -129,7 +165,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
do_cache = kwargs.pop('cache', True)
|
||||
|
||||
self.clean(**kwargs)
|
||||
self.validate_unique(**kwargs)
|
||||
self.validate_unique()
|
||||
|
||||
# Execute before_save action
|
||||
self._call_settings_function('before_save', args, kwargs)
|
||||
@ -162,7 +198,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
@property
|
||||
def cache_key(self):
|
||||
"""Generate a unique cache key for this settings object"""
|
||||
return self.__class__.create_cache_key(self.key, **self.get_kwargs())
|
||||
return self.__class__.create_cache_key(self.key, **self.get_filters_for_instance())
|
||||
|
||||
def save_to_cache(self):
|
||||
"""Save this setting object to cache"""
|
||||
@ -199,7 +235,16 @@ class BaseInvenTreeSetting(models.Model):
|
||||
return key.replace(" ", "")
|
||||
|
||||
@classmethod
|
||||
def allValues(cls, user=None, exclude_hidden=False):
|
||||
def get_filters(cls, **kwargs):
|
||||
"""Enable to filter by other kwargs defined in cls.extra_unique_fields"""
|
||||
return {key: value for key, value in kwargs.items() if key in cls.extra_unique_fields}
|
||||
|
||||
def get_filters_for_instance(self):
|
||||
"""Enable to filter by other fields defined in self.extra_unique_fields"""
|
||||
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.
|
||||
|
||||
This performs a single database lookup,
|
||||
@ -212,9 +257,8 @@ class BaseInvenTreeSetting(models.Model):
|
||||
# Keys which start with an undersore are used for internal functionality
|
||||
results = results.exclude(key__startswith='_')
|
||||
|
||||
# Optionally filter by user
|
||||
if user is not None:
|
||||
results = results.filter(user=user)
|
||||
# Optionally filter by other keys
|
||||
results = results.filter(**cls.get_filters(**kwargs))
|
||||
|
||||
# Query the database
|
||||
settings = {}
|
||||
@ -253,16 +297,6 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
return settings
|
||||
|
||||
def get_kwargs(self):
|
||||
"""Construct kwargs for doing class-based settings lookup, depending on *which* class we are.
|
||||
|
||||
This is necessary to abtract the settings object
|
||||
from the implementing class (e.g plugins)
|
||||
|
||||
Subclasses should override this function to ensure the kwargs are correctly set.
|
||||
"""
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def get_setting_definition(cls, key, **kwargs):
|
||||
"""Return the 'definition' of a particular settings value, as a dict object.
|
||||
@ -361,32 +395,11 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
filters = {
|
||||
'key__iexact': key,
|
||||
|
||||
# Optionally filter by other keys
|
||||
**cls.get_filters(**kwargs),
|
||||
}
|
||||
|
||||
# Filter by user
|
||||
user = kwargs.get('user', None)
|
||||
|
||||
if user is not None:
|
||||
filters['user'] = user
|
||||
|
||||
# Filter by plugin
|
||||
plugin = kwargs.get('plugin', None)
|
||||
|
||||
if plugin is not None:
|
||||
from plugin import InvenTreePlugin
|
||||
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
plugin = plugin.plugin_config()
|
||||
|
||||
filters['plugin'] = plugin
|
||||
kwargs['plugin'] = plugin
|
||||
|
||||
# Filter by method
|
||||
method = kwargs.get('method', None)
|
||||
|
||||
if method is not None:
|
||||
filters['method'] = method
|
||||
|
||||
# Perform cache lookup by default
|
||||
do_cache = kwargs.pop('cache', True)
|
||||
|
||||
@ -493,22 +506,11 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
filters = {
|
||||
'key__iexact': key,
|
||||
|
||||
# Optionally filter by other keys
|
||||
**cls.get_filters(**kwargs),
|
||||
}
|
||||
|
||||
user = kwargs.get('user', None)
|
||||
plugin = kwargs.get('plugin', None)
|
||||
|
||||
if user is not None:
|
||||
filters['user'] = user
|
||||
|
||||
if plugin is not None:
|
||||
from plugin import InvenTreePlugin
|
||||
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
filters['plugin'] = plugin.plugin_config()
|
||||
else:
|
||||
filters['plugin'] = plugin
|
||||
|
||||
try:
|
||||
setting = cls.objects.get(**filters)
|
||||
except cls.DoesNotExist:
|
||||
@ -532,22 +534,22 @@ class BaseInvenTreeSetting(models.Model):
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name for setting."""
|
||||
return self.__class__.get_setting_name(self.key, **self.get_kwargs())
|
||||
return self.__class__.get_setting_name(self.key, **self.get_filters_for_instance())
|
||||
|
||||
@property
|
||||
def default_value(self):
|
||||
"""Return default_value for setting."""
|
||||
return self.__class__.get_setting_default(self.key, **self.get_kwargs())
|
||||
return self.__class__.get_setting_default(self.key, **self.get_filters_for_instance())
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Return description for setting."""
|
||||
return self.__class__.get_setting_description(self.key, **self.get_kwargs())
|
||||
return self.__class__.get_setting_description(self.key, **self.get_filters_for_instance())
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
"""Return units for setting."""
|
||||
return self.__class__.get_setting_units(self.key, **self.get_kwargs())
|
||||
return self.__class__.get_setting_units(self.key, **self.get_filters_for_instance())
|
||||
|
||||
def clean(self, **kwargs):
|
||||
"""If a validator (or multiple validators) are defined for a particular setting key, run them against the 'value' field."""
|
||||
@ -615,7 +617,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
validator(value)
|
||||
|
||||
def validate_unique(self, exclude=None, **kwargs):
|
||||
def validate_unique(self, exclude=None):
|
||||
"""Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' is unique, using a case-insensitive comparison.
|
||||
|
||||
Note that sub-classes (UserSetting, PluginSetting) use other filters
|
||||
@ -625,17 +627,11 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
filters = {
|
||||
'key__iexact': self.key,
|
||||
|
||||
# Optionally filter by other keys
|
||||
**self.get_filters_for_instance(),
|
||||
}
|
||||
|
||||
user = getattr(self, 'user', None)
|
||||
plugin = getattr(self, 'plugin', None)
|
||||
|
||||
if user is not None:
|
||||
filters['user'] = user
|
||||
|
||||
if plugin is not None:
|
||||
filters['plugin'] = plugin
|
||||
|
||||
try:
|
||||
# Check if a duplicate setting already exists
|
||||
setting = self.__class__.objects.filter(**filters).exclude(id=self.id)
|
||||
@ -648,7 +644,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
def choices(self):
|
||||
"""Return the available choices for this setting (or None if no choices are defined)."""
|
||||
return self.__class__.get_setting_choices(self.key, **self.get_kwargs())
|
||||
return self.__class__.get_setting_choices(self.key, **self.get_filters_for_instance())
|
||||
|
||||
def valid_options(self):
|
||||
"""Return a list of valid options for this setting."""
|
||||
@ -661,7 +657,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
def is_choice(self):
|
||||
"""Check if this setting is a "choice" field."""
|
||||
return self.__class__.get_setting_choices(self.key, **self.get_kwargs()) is not None
|
||||
return self.__class__.get_setting_choices(self.key, **self.get_filters_for_instance()) is not None
|
||||
|
||||
def as_choice(self):
|
||||
"""Render this setting as the "display" value of a choice field.
|
||||
@ -671,7 +667,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
and the value is 'A4',
|
||||
then display 'A4 paper'
|
||||
"""
|
||||
choices = self.get_setting_choices(self.key, **self.get_kwargs())
|
||||
choices = self.get_setting_choices(self.key, **self.get_filters_for_instance())
|
||||
|
||||
if not choices:
|
||||
return self.value
|
||||
@ -688,7 +684,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
def model_name(self):
|
||||
"""Return the model name associated with this setting."""
|
||||
setting = self.get_setting_definition(self.key, **self.get_kwargs())
|
||||
setting = self.get_setting_definition(self.key, **self.get_filters_for_instance())
|
||||
|
||||
return setting.get('model', None)
|
||||
|
||||
@ -752,7 +748,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
def is_bool(self):
|
||||
"""Check if this setting is required to be a boolean value."""
|
||||
validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs())
|
||||
validator = self.__class__.get_setting_validator(self.key, **self.get_filters_for_instance())
|
||||
|
||||
return self.__class__.validator_is_bool(validator)
|
||||
|
||||
@ -792,7 +788,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
def is_int(self,):
|
||||
"""Check if the setting is required to be an integer value."""
|
||||
validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs())
|
||||
validator = self.__class__.get_setting_validator(self.key, **self.get_filters_for_instance())
|
||||
|
||||
return self.__class__.validator_is_int(validator)
|
||||
|
||||
@ -831,7 +827,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
@property
|
||||
def protected(self):
|
||||
"""Returns if setting is protected from rendering."""
|
||||
return self.__class__.is_protected(self.key, **self.get_kwargs())
|
||||
return self.__class__.is_protected(self.key, **self.get_filters_for_instance())
|
||||
|
||||
|
||||
def settings_group_options():
|
||||
@ -2105,6 +2101,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
}
|
||||
|
||||
typ = 'user'
|
||||
extra_unique_fields = ['user']
|
||||
|
||||
key = models.CharField(
|
||||
max_length=50,
|
||||
@ -2121,20 +2118,10 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
help_text=_('User'),
|
||||
)
|
||||
|
||||
def validate_unique(self, exclude=None, **kwargs):
|
||||
"""Return if the setting (including key) is unique."""
|
||||
return super().validate_unique(exclude=exclude, user=self.user)
|
||||
|
||||
def to_native_value(self):
|
||||
"""Return the "pythonic" value, e.g. convert "True" to True, and "1" to 1."""
|
||||
return self.__class__.get_setting(self.key, user=self.user)
|
||||
|
||||
def get_kwargs(self):
|
||||
"""Explicit kwargs required to uniquely identify a particular setting object, in addition to the 'key' parameter."""
|
||||
return {
|
||||
'user': self.user,
|
||||
}
|
||||
|
||||
|
||||
class PriceBreak(MetaMixin):
|
||||
"""Represents a PriceBreak model."""
|
||||
|
@ -19,6 +19,7 @@ from common.settings import currency_code_default
|
||||
from InvenTree import settings, version
|
||||
from plugin import registry
|
||||
from plugin.models import NotificationUserSetting, PluginSetting
|
||||
from plugin.plugin import InvenTreePlugin
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@ -325,6 +326,8 @@ def setting_object(key, *args, **kwargs):
|
||||
# Note, 'plugin' is an instance of an InvenTreePlugin class
|
||||
|
||||
plugin = kwargs['plugin']
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
plugin = plugin.plugin_config()
|
||||
|
||||
return PluginSetting.get_setting_object(key, plugin=plugin, cache=cache)
|
||||
|
||||
|
@ -232,7 +232,7 @@ class PluginSettingDetail(RetrieveUpdateAPI):
|
||||
if key not in settings:
|
||||
raise NotFound(detail=f"Plugin '{plugin.slug}' has no setting matching '{key}'")
|
||||
|
||||
return PluginSetting.get_setting_object(key, plugin=plugin)
|
||||
return PluginSetting.get_setting_object(key, plugin=plugin.plugin_config())
|
||||
|
||||
# Staff permission required
|
||||
permission_classes = [
|
||||
|
@ -1,14 +1,25 @@
|
||||
"""Plugin mixin class for SettingsMixin."""
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
# import only for typechecking, otherwise this throws a model is unready error
|
||||
if TYPE_CHECKING:
|
||||
from common.models import SettingsKeyType
|
||||
else:
|
||||
class SettingsKeyType:
|
||||
"""Dummy class, so that python throws no error"""
|
||||
pass
|
||||
|
||||
|
||||
class SettingsMixin:
|
||||
"""Mixin that enables global settings for the plugin."""
|
||||
|
||||
SETTINGS: Dict[str, SettingsKeyType] = {}
|
||||
|
||||
class MixinMeta:
|
||||
"""Meta for mixin."""
|
||||
MIXIN_NAME = 'Settings'
|
||||
@ -56,7 +67,7 @@ class SettingsMixin:
|
||||
"""
|
||||
from plugin.models import PluginSetting
|
||||
|
||||
return PluginSetting.get_setting(key, plugin=self, cache=cache)
|
||||
return PluginSetting.get_setting(key, plugin=self.plugin_config(), cache=cache)
|
||||
|
||||
def set_setting(self, key, value, user=None):
|
||||
"""Set plugin setting value by key."""
|
||||
|
@ -133,6 +133,7 @@ class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||
"""This model represents settings for individual plugins."""
|
||||
|
||||
typ = 'plugin'
|
||||
extra_unique_fields = ['plugin']
|
||||
|
||||
class Meta:
|
||||
"""Meta for PluginSetting."""
|
||||
@ -165,27 +166,18 @@ class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||
plugin = kwargs.pop('plugin', None)
|
||||
|
||||
if plugin:
|
||||
|
||||
if issubclass(plugin.__class__, InvenTreePlugin):
|
||||
plugin = plugin.plugin_config()
|
||||
|
||||
mixin_settings = getattr(registry, 'mixins_settings')
|
||||
if mixin_settings:
|
||||
kwargs['settings'] = mixin_settings.get(plugin.key, {})
|
||||
|
||||
return super().get_setting_definition(key, **kwargs)
|
||||
|
||||
def get_kwargs(self):
|
||||
"""Explicit kwargs required to uniquely identify a particular setting object, in addition to the 'key' parameter."""
|
||||
return {
|
||||
'plugin': self.plugin,
|
||||
}
|
||||
|
||||
|
||||
class NotificationUserSetting(common.models.BaseInvenTreeSetting):
|
||||
"""This model represents notification settings for a user."""
|
||||
|
||||
typ = 'notification'
|
||||
extra_unique_fields = ['method', 'user']
|
||||
|
||||
class Meta:
|
||||
"""Meta for NotificationUserSetting."""
|
||||
@ -202,13 +194,6 @@ class NotificationUserSetting(common.models.BaseInvenTreeSetting):
|
||||
|
||||
return super().get_setting_definition(key, **kwargs)
|
||||
|
||||
def get_kwargs(self):
|
||||
"""Explicit kwargs required to uniquely identify a particular setting object, in addition to the 'key' parameter."""
|
||||
return {
|
||||
'method': self.method,
|
||||
'user': self.user,
|
||||
}
|
||||
|
||||
method = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_('Method'),
|
||||
|
Loading…
Reference in New Issue
Block a user