mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2981 from SchrodingersGat/model-setting-reference
Add settings which reference *model instances* allowing better lookup of models
This commit is contained in:
commit
c2dd8afea6
@ -17,15 +17,16 @@ import base64
|
||||
from secrets import compare_digest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models, transaction
|
||||
from django.db.utils import IntegrityError, OperationalError
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.utils import IntegrityError, OperationalError
|
||||
from django.conf import settings
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
|
||||
from djmoney.settings import CURRENCY_CHOICES
|
||||
from djmoney.contrib.exchange.models import convert_money
|
||||
@ -136,6 +137,19 @@ 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):
|
||||
"""
|
||||
@ -319,11 +333,11 @@ class BaseInvenTreeSetting(models.Model):
|
||||
value = setting.value
|
||||
|
||||
# Cast to boolean if necessary
|
||||
if setting.is_bool(**kwargs):
|
||||
if setting.is_bool():
|
||||
value = InvenTree.helpers.str2bool(value)
|
||||
|
||||
# Cast to integer if necessary
|
||||
if setting.is_int(**kwargs):
|
||||
if setting.is_int():
|
||||
try:
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
@ -390,19 +404,19 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.get_setting_name(self.key)
|
||||
return self.__class__.get_setting_name(self.key, **self.get_kwargs())
|
||||
|
||||
@property
|
||||
def default_value(self):
|
||||
return self.__class__.get_setting_default(self.key)
|
||||
return self.__class__.get_setting_default(self.key, **self.get_kwargs())
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.__class__.get_setting_description(self.key)
|
||||
return self.__class__.get_setting_description(self.key, **self.get_kwargs())
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return self.__class__.get_setting_units(self.key)
|
||||
return self.__class__.get_setting_units(self.key, **self.get_kwargs())
|
||||
|
||||
def clean(self, **kwargs):
|
||||
"""
|
||||
@ -512,12 +526,12 @@ class BaseInvenTreeSetting(models.Model):
|
||||
except self.DoesNotExist:
|
||||
pass
|
||||
|
||||
def choices(self, **kwargs):
|
||||
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, **kwargs)
|
||||
return self.__class__.get_setting_choices(self.key, **self.get_kwargs())
|
||||
|
||||
def valid_options(self):
|
||||
"""
|
||||
@ -531,14 +545,14 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
return [opt[0] for opt in choices]
|
||||
|
||||
def is_choice(self, **kwargs):
|
||||
def is_choice(self):
|
||||
"""
|
||||
Check if this setting is a "choice" field
|
||||
"""
|
||||
|
||||
return self.__class__.get_setting_choices(self.key, **kwargs) is not None
|
||||
return self.__class__.get_setting_choices(self.key, **self.get_kwargs()) is not None
|
||||
|
||||
def as_choice(self, **kwargs):
|
||||
def as_choice(self):
|
||||
"""
|
||||
Render this setting as the "display" value of a choice field,
|
||||
e.g. if the choices are:
|
||||
@ -547,7 +561,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
then display 'A4 paper'
|
||||
"""
|
||||
|
||||
choices = self.get_setting_choices(self.key, **kwargs)
|
||||
choices = self.get_setting_choices(self.key, **self.get_kwargs())
|
||||
|
||||
if not choices:
|
||||
return self.value
|
||||
@ -558,12 +572,80 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
return self.value
|
||||
|
||||
def is_bool(self, **kwargs):
|
||||
def is_model(self):
|
||||
"""
|
||||
Check if this setting references a model instance in the database
|
||||
"""
|
||||
|
||||
return self.model_name() is not None
|
||||
|
||||
def model_name(self):
|
||||
"""
|
||||
Return the model name associated with this setting
|
||||
"""
|
||||
|
||||
setting = self.get_setting_definition(self.key, **self.get_kwargs())
|
||||
|
||||
return setting.get('model', None)
|
||||
|
||||
def model_class(self):
|
||||
"""
|
||||
Return the model class associated with this setting, if (and only if):
|
||||
|
||||
- It has a defined 'model' parameter
|
||||
- The 'model' parameter is of the form app.model
|
||||
- The 'model' parameter has matches a known app model
|
||||
"""
|
||||
|
||||
model_name = self.model_name()
|
||||
|
||||
if not model_name:
|
||||
return None
|
||||
|
||||
try:
|
||||
(app, mdl) = model_name.strip().split('.')
|
||||
except ValueError:
|
||||
logger.error(f"Invalid 'model' parameter for setting {self.key} : '{model_name}'")
|
||||
return None
|
||||
|
||||
app_models = apps.all_models.get(app, None)
|
||||
|
||||
if app_models is None:
|
||||
logger.error(f"Error retrieving model class '{model_name}' for setting '{self.key}' - no app named '{app}'")
|
||||
return None
|
||||
|
||||
model = app_models.get(mdl, None)
|
||||
|
||||
if model is None:
|
||||
logger.error(f"Error retrieving model class '{model_name}' for setting '{self.key}' - no model named '{mdl}'")
|
||||
return None
|
||||
|
||||
# Looks like we have found a model!
|
||||
return model
|
||||
|
||||
def api_url(self):
|
||||
"""
|
||||
Return the API url associated with the linked model,
|
||||
if provided, and valid!
|
||||
"""
|
||||
|
||||
model_class = self.model_class()
|
||||
|
||||
if model_class:
|
||||
# If a valid class has been found, see if it has registered an API URL
|
||||
try:
|
||||
return model_class.get_api_url()
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def is_bool(self):
|
||||
"""
|
||||
Check if this setting is required to be a boolean value
|
||||
"""
|
||||
|
||||
validator = self.__class__.get_setting_validator(self.key, **kwargs)
|
||||
validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs())
|
||||
|
||||
return self.__class__.validator_is_bool(validator)
|
||||
|
||||
@ -576,17 +658,20 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
return InvenTree.helpers.str2bool(self.value)
|
||||
|
||||
def setting_type(self, **kwargs):
|
||||
def setting_type(self):
|
||||
"""
|
||||
Return the field type identifier for this setting object
|
||||
"""
|
||||
|
||||
if self.is_bool(**kwargs):
|
||||
if self.is_bool():
|
||||
return 'boolean'
|
||||
|
||||
elif self.is_int(**kwargs):
|
||||
elif self.is_int():
|
||||
return 'integer'
|
||||
|
||||
elif self.is_model():
|
||||
return 'related field'
|
||||
|
||||
else:
|
||||
return 'string'
|
||||
|
||||
@ -603,12 +688,12 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
return False
|
||||
|
||||
def is_int(self, **kwargs):
|
||||
def is_int(self,):
|
||||
"""
|
||||
Check if the setting is required to be an integer value:
|
||||
"""
|
||||
|
||||
validator = self.__class__.get_setting_validator(self.key, **kwargs)
|
||||
validator = self.__class__.get_setting_validator(self.key, **self.get_kwargs())
|
||||
|
||||
return self.__class__.validator_is_int(validator)
|
||||
|
||||
@ -651,88 +736,7 @@ class BaseInvenTreeSetting(models.Model):
|
||||
|
||||
@property
|
||||
def protected(self):
|
||||
return self.__class__.is_protected(self.key)
|
||||
|
||||
|
||||
class GenericReferencedSettingClass:
|
||||
"""
|
||||
This mixin can be used to add reference keys to static properties
|
||||
|
||||
Sample:
|
||||
```python
|
||||
class SampleSetting(GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
||||
class Meta:
|
||||
unique_together = [
|
||||
('sample', 'key'),
|
||||
]
|
||||
|
||||
REFERENCE_NAME = 'sample'
|
||||
|
||||
@classmethod
|
||||
def get_setting_definition(cls, key, **kwargs):
|
||||
# mysampledict contains the dict with all settings for this SettingClass - this could also be a dynamic lookup
|
||||
|
||||
kwargs['settings'] = mysampledict
|
||||
return super().get_setting_definition(key, **kwargs)
|
||||
|
||||
sample = models.charKey( # the name for this field is the additonal key and must be set in the Meta class an REFERENCE_NAME
|
||||
max_length=256,
|
||||
verbose_name=_('sample')
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
REFERENCE_NAME = None
|
||||
|
||||
def _get_reference(self):
|
||||
"""
|
||||
Returns dict that can be used as an argument for kwargs calls.
|
||||
Helps to make overriden calls generic for simple reuse.
|
||||
|
||||
Usage:
|
||||
```python
|
||||
some_random_function(argument0, kwarg1=value1, **self._get_reference())
|
||||
```
|
||||
"""
|
||||
return {
|
||||
self.REFERENCE_NAME: getattr(self, self.REFERENCE_NAME)
|
||||
}
|
||||
|
||||
"""
|
||||
We override the following class methods,
|
||||
so that we can pass the modified key instance as an additional argument
|
||||
"""
|
||||
|
||||
def clean(self, **kwargs):
|
||||
|
||||
kwargs[self.REFERENCE_NAME] = getattr(self, self.REFERENCE_NAME)
|
||||
|
||||
super().clean(**kwargs)
|
||||
|
||||
def is_bool(self, **kwargs):
|
||||
|
||||
kwargs[self.REFERENCE_NAME] = getattr(self, self.REFERENCE_NAME)
|
||||
|
||||
return super().is_bool(**kwargs)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__class__.get_setting_name(self.key, **self._get_reference())
|
||||
|
||||
@property
|
||||
def default_value(self):
|
||||
return self.__class__.get_setting_default(self.key, **self._get_reference())
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.__class__.get_setting_description(self.key, **self._get_reference())
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return self.__class__.get_setting_units(self.key, **self._get_reference())
|
||||
|
||||
def choices(self):
|
||||
return self.__class__.get_setting_choices(self.key, **self._get_reference())
|
||||
return self.__class__.is_protected(self.key, **self.get_kwargs())
|
||||
|
||||
|
||||
def settings_group_options():
|
||||
@ -1558,6 +1562,16 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
||||
|
||||
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(models.Model):
|
||||
"""
|
||||
|
@ -28,6 +28,10 @@ class SettingsSerializer(InvenTreeModelSerializer):
|
||||
|
||||
choices = serializers.SerializerMethodField()
|
||||
|
||||
model_name = serializers.CharField(read_only=True)
|
||||
|
||||
api_url = serializers.CharField(read_only=True)
|
||||
|
||||
def get_choices(self, obj):
|
||||
"""
|
||||
Returns the choices available for a given item
|
||||
@ -75,6 +79,8 @@ class GlobalSettingsSerializer(SettingsSerializer):
|
||||
'description',
|
||||
'type',
|
||||
'choices',
|
||||
'model_name',
|
||||
'api_url',
|
||||
]
|
||||
|
||||
|
||||
@ -96,6 +102,8 @@ class UserSettingsSerializer(SettingsSerializer):
|
||||
'user',
|
||||
'type',
|
||||
'choices',
|
||||
'model_name',
|
||||
'api_url',
|
||||
]
|
||||
|
||||
|
||||
@ -124,6 +132,8 @@ class GenericReferencedSettingSerializer(SettingsSerializer):
|
||||
'description',
|
||||
'type',
|
||||
'choices',
|
||||
'model_name',
|
||||
'api_url',
|
||||
]
|
||||
|
||||
# set Meta class
|
||||
|
@ -102,7 +102,7 @@ class PluginConfig(models.Model):
|
||||
return ret
|
||||
|
||||
|
||||
class PluginSetting(common.models.GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
||||
class PluginSetting(common.models.BaseInvenTreeSetting):
|
||||
"""
|
||||
This model represents settings for individual plugins
|
||||
"""
|
||||
@ -112,7 +112,13 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B
|
||||
('plugin', 'key'),
|
||||
]
|
||||
|
||||
REFERENCE_NAME = 'plugin'
|
||||
plugin = models.ForeignKey(
|
||||
PluginConfig,
|
||||
related_name='settings',
|
||||
null=False,
|
||||
verbose_name=_('Plugin'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_setting_definition(cls, key, **kwargs):
|
||||
@ -142,16 +148,18 @@ class PluginSetting(common.models.GenericReferencedSettingClass, common.models.B
|
||||
|
||||
return super().get_setting_definition(key, **kwargs)
|
||||
|
||||
plugin = models.ForeignKey(
|
||||
PluginConfig,
|
||||
related_name='settings',
|
||||
null=False,
|
||||
verbose_name=_('Plugin'),
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
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.GenericReferencedSettingClass, common.models.BaseInvenTreeSetting):
|
||||
class NotificationUserSetting(common.models.BaseInvenTreeSetting):
|
||||
"""
|
||||
This model represents notification settings for a user
|
||||
"""
|
||||
@ -161,8 +169,6 @@ class NotificationUserSetting(common.models.GenericReferencedSettingClass, commo
|
||||
('method', 'user', 'key'),
|
||||
]
|
||||
|
||||
REFERENCE_NAME = 'method'
|
||||
|
||||
@classmethod
|
||||
def get_setting_definition(cls, key, **kwargs):
|
||||
from common.notifications import storage
|
||||
@ -171,6 +177,17 @@ class NotificationUserSetting(common.models.GenericReferencedSettingClass, commo
|
||||
|
||||
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'),
|
||||
|
@ -65,6 +65,16 @@ class SampleIntegrationPlugin(AppMixin, SettingsMixin, UrlsMixin, NavigationMixi
|
||||
],
|
||||
'default': 'A',
|
||||
},
|
||||
'SELECT_COMPANY': {
|
||||
'name': 'Company',
|
||||
'description': 'Select a company object from the database',
|
||||
'model': 'company.company',
|
||||
},
|
||||
'SELECT_PART': {
|
||||
'name': 'Part',
|
||||
'description': 'Select a part object from the database',
|
||||
'model': 'part.part',
|
||||
},
|
||||
}
|
||||
|
||||
NAVIGATION = [
|
||||
|
@ -71,9 +71,24 @@ function editSetting(key, options={}) {
|
||||
help_text: response.description,
|
||||
type: response.type,
|
||||
choices: response.choices,
|
||||
value: response.value,
|
||||
}
|
||||
};
|
||||
|
||||
// Foreign key lookup available!
|
||||
if (response.type == 'related field') {
|
||||
|
||||
if (response.model_name && response.api_url) {
|
||||
fields.value.type = 'related field';
|
||||
fields.value.model = response.model_name.split('.').at(-1);
|
||||
fields.value.api_url = response.api_url;
|
||||
} else {
|
||||
// Unknown / unsupported model type, default to 'text' field
|
||||
fields.value.type = 'text';
|
||||
console.warn(`Unsupported model type: '${response.model_name}' for setting '${response.key}'`);
|
||||
}
|
||||
}
|
||||
|
||||
constructChangeForm(fields, {
|
||||
url: url,
|
||||
method: 'PATCH',
|
||||
|
Loading…
Reference in New Issue
Block a user