diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 3d18d56880..02d752aa8c 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -11,6 +11,7 @@ import decimal import math from django.db import models, transaction +from django.contrib.auth.models import User from django.db.utils import IntegrityError, OperationalError from django.conf import settings @@ -28,7 +29,394 @@ import InvenTree.helpers import InvenTree.fields -class InvenTreeSetting(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). + """ + + + GLOBAL_SETTINGS = {} + + class Meta: + abstract = True + + @classmethod + def get_setting_name(cls, key): + """ + Return the name of a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('name', '') + else: + return '' + + @classmethod + def get_setting_description(cls, key): + """ + Return the description for a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('description', '') + else: + return '' + + @classmethod + def get_setting_units(cls, key): + """ + Return the units for a particular setting. + + If it does not exist, return an empty string. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('units', '') + else: + return '' + + @classmethod + def get_setting_validator(cls, key): + """ + Return the validator for a particular setting. + + If it does not exist, return None + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('validator', None) + else: + return None + + @classmethod + def get_setting_default(cls, key): + """ + Return the default value for a particular setting. + + If it does not exist, return an empty string + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + return setting.get('default', '') + else: + return '' + + @classmethod + def get_setting_choices(cls, key): + """ + Return the validator choices available for a particular setting. + """ + + key = str(key).strip().upper() + + if key in cls.GLOBAL_SETTINGS: + setting = cls.GLOBAL_SETTINGS[key] + choices = setting.get('choices', None) + else: + choices = None + + """ + TODO: + if type(choices) is function: + # Evaluate the function (we expect it will return a list of tuples...) + return choices() + """ + + return choices + + @classmethod + def get_setting_object(cls, key): + """ + Return an InvenTreeSetting object matching the given key. + + - Key is case-insensitive + - Returns None if no match is made + """ + + key = str(key).strip().upper() + + try: + setting = cls.objects.filter(key__iexact=key).first() + except (ValueError, cls.DoesNotExist): + setting = None + except (IntegrityError, OperationalError): + setting = None + + # Setting does not exist! (Try to create it) + if not setting: + + setting = cls(key=key, value=cls.get_setting_default(key)) + + try: + # Wrap this statement in "atomic", so it can be rolled back if it fails + with transaction.atomic(): + setting.save() + except (IntegrityError, OperationalError): + # It might be the case that the database isn't created yet + pass + + return setting + + @classmethod + def get_setting_pk(cls, key): + """ + Return the primary-key value for a given setting. + + If the setting does not exist, return None + """ + + setting = cls.get_setting_object(cls) + + if setting: + return setting.pk + else: + return None + + @classmethod + def get_setting(cls, key, backup_value=None): + """ + Get the value of a particular setting. + If it does not exist, return the backup value (default = None) + """ + + # If no backup value is specified, atttempt to retrieve a "default" value + if backup_value is None: + backup_value = cls.get_setting_default(key) + + setting = cls.get_setting_object(key) + + if setting: + value = setting.value + + # If the particular setting is defined as a boolean, cast the value to a boolean + if setting.is_bool(): + value = InvenTree.helpers.str2bool(value) + + if setting.is_int(): + try: + value = int(value) + except (ValueError, TypeError): + value = backup_value + + else: + value = backup_value + + return value + + @classmethod + def set_setting(cls, key, value, user, create=True): + """ + Set the value of a particular setting. + If it does not exist, option to create it. + + Args: + key: settings key + value: New value + user: User object (must be staff member to update a core setting) + create: If True, create a new setting if the specified key does not exist. + """ + + if user is not None and not user.is_staff: + return + + try: + setting = cls.objects.get(key__iexact=key) + except cls.DoesNotExist: + + if create: + setting = cls(key=key) + else: + return + + # Enforce standard boolean representation + if setting.is_bool(): + value = InvenTree.helpers.str2bool(value) + + setting.value = str(value) + setting.save() + + key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) + + value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) + + @property + def name(self): + return self.__class__.get_setting_name(self.key) + + @property + def default_value(self): + return self.__class__.get_setting_default(self.key) + + @property + def description(self): + return self.__class__.get_setting_description(self.key) + + @property + def units(self): + return self.__class__.get_setting_units(self.key) + + def clean(self): + """ + If a validator (or multiple validators) are defined for a particular setting key, + run them against the 'value' field. + """ + + super().clean() + + validator = self.__class__.get_setting_validator(self.key) + + if self.is_bool(): + self.value = InvenTree.helpers.str2bool(self.value) + + if self.is_int(): + try: + self.value = int(self.value) + except (ValueError): + raise ValidationError(_('Must be an integer value')) + + if validator is not None: + self.run_validator(validator) + + def run_validator(self, validator): + """ + Run a validator against the 'value' field for this InvenTreeSetting object. + """ + + if validator is None: + return + + value = self.value + + # Boolean validator + if self.is_bool(): + # Value must "look like" a boolean value + if InvenTree.helpers.is_bool(value): + # Coerce into either "True" or "False" + value = InvenTree.helpers.str2bool(value) + else: + raise ValidationError({ + 'value': _('Value must be a boolean value') + }) + + # Integer validator + if self.is_int(): + + try: + # Coerce into an integer value + value = int(value) + except (ValueError, TypeError): + raise ValidationError({ + 'value': _('Value must be an integer value'), + }) + + # If a list of validators is supplied, iterate through each one + if type(validator) in [list, tuple]: + for v in validator: + self.run_validator(v) + + if callable(validator): + # We can accept function validators with a single argument + validator(self.value) + + 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. + """ + + super().validate_unique(exclude) + + try: + setting = self.__class__.objects.exclude(id=self.id).filter(key__iexact=self.key) + if setting.exists(): + raise ValidationError({'key': _('Key string must be unique')}) + except self.DoesNotExist: + pass + + 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) + + def is_bool(self): + """ + Check if this setting is required to be a boolean value + """ + + validator = self.__class__.get_setting_validator(self.key) + + if validator == bool: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == bool: + return True + + def as_bool(self): + """ + Return the value of this setting converted to a boolean value. + + Warning: Only use on values where is_bool evaluates to true! + """ + + return InvenTree.helpers.str2bool(self.value) + + def is_int(self): + """ + Check if the setting is required to be an integer value: + """ + + validator = self.__class__.get_setting_validator(self.key) + + if validator == int: + return True + + if type(validator) in [list, tuple]: + for v in validator: + if v == int: + return True + + return False + + def as_int(self): + """ + Return the value of this setting converted to a boolean value. + + If an error occurs, return the default value + """ + + try: + value = int(self.value) + except (ValueError, TypeError): + value = self.default_value() + + return value + + +class InvenTreeSetting(BaseInvenTreeSetting): """ An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values). @@ -357,380 +745,6 @@ class InvenTreeSetting(models.Model): verbose_name = "InvenTree Setting" verbose_name_plural = "InvenTree Settings" - @classmethod - def get_setting_name(cls, key): - """ - Return the name of a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('name', '') - else: - return '' - - @classmethod - def get_setting_description(cls, key): - """ - Return the description for a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('description', '') - else: - return '' - - @classmethod - def get_setting_units(cls, key): - """ - Return the units for a particular setting. - - If it does not exist, return an empty string. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('units', '') - else: - return '' - - @classmethod - def get_setting_validator(cls, key): - """ - Return the validator for a particular setting. - - If it does not exist, return None - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('validator', None) - else: - return None - - @classmethod - def get_setting_default(cls, key): - """ - Return the default value for a particular setting. - - If it does not exist, return an empty string - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - return setting.get('default', '') - else: - return '' - - @classmethod - def get_setting_choices(cls, key): - """ - Return the validator choices available for a particular setting. - """ - - key = str(key).strip().upper() - - if key in cls.GLOBAL_SETTINGS: - setting = cls.GLOBAL_SETTINGS[key] - choices = setting.get('choices', None) - else: - choices = None - - """ - TODO: - if type(choices) is function: - # Evaluate the function (we expect it will return a list of tuples...) - return choices() - """ - - return choices - - @classmethod - def get_setting_object(cls, key): - """ - Return an InvenTreeSetting object matching the given key. - - - Key is case-insensitive - - Returns None if no match is made - """ - - key = str(key).strip().upper() - - try: - setting = InvenTreeSetting.objects.filter(key__iexact=key).first() - except (ValueError, InvenTreeSetting.DoesNotExist): - setting = None - except (IntegrityError, OperationalError): - setting = None - - # Setting does not exist! (Try to create it) - if not setting: - - setting = InvenTreeSetting(key=key, value=InvenTreeSetting.get_setting_default(key)) - - try: - # Wrap this statement in "atomic", so it can be rolled back if it fails - with transaction.atomic(): - setting.save() - except (IntegrityError, OperationalError): - # It might be the case that the database isn't created yet - pass - - return setting - - @classmethod - def get_setting_pk(cls, key): - """ - Return the primary-key value for a given setting. - - If the setting does not exist, return None - """ - - setting = InvenTreeSetting.get_setting_object(cls) - - if setting: - return setting.pk - else: - return None - - @classmethod - def get_setting(cls, key, backup_value=None): - """ - Get the value of a particular setting. - If it does not exist, return the backup value (default = None) - """ - - # If no backup value is specified, atttempt to retrieve a "default" value - if backup_value is None: - backup_value = cls.get_setting_default(key) - - setting = InvenTreeSetting.get_setting_object(key) - - if setting: - value = setting.value - - # If the particular setting is defined as a boolean, cast the value to a boolean - if setting.is_bool(): - value = InvenTree.helpers.str2bool(value) - - if setting.is_int(): - try: - value = int(value) - except (ValueError, TypeError): - value = backup_value - - else: - value = backup_value - - return value - - @classmethod - def set_setting(cls, key, value, user, create=True): - """ - Set the value of a particular setting. - If it does not exist, option to create it. - - Args: - key: settings key - value: New value - user: User object (must be staff member to update a core setting) - create: If True, create a new setting if the specified key does not exist. - """ - - if user is not None and not user.is_staff: - return - - try: - setting = InvenTreeSetting.objects.get(key__iexact=key) - except InvenTreeSetting.DoesNotExist: - - if create: - setting = InvenTreeSetting(key=key) - else: - return - - # Enforce standard boolean representation - if setting.is_bool(): - value = InvenTree.helpers.str2bool(value) - - setting.value = str(value) - setting.save() - - key = models.CharField(max_length=50, blank=False, unique=True, help_text=_('Settings key (must be unique - case insensitive')) - - value = models.CharField(max_length=200, blank=True, unique=False, help_text=_('Settings value')) - - @property - def name(self): - return InvenTreeSetting.get_setting_name(self.key) - - @property - def default_value(self): - return InvenTreeSetting.get_setting_default(self.key) - - @property - def description(self): - return InvenTreeSetting.get_setting_description(self.key) - - @property - def units(self): - return InvenTreeSetting.get_setting_units(self.key) - - def clean(self): - """ - If a validator (or multiple validators) are defined for a particular setting key, - run them against the 'value' field. - """ - - super().clean() - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if self.is_bool(): - self.value = InvenTree.helpers.str2bool(self.value) - - if self.is_int(): - try: - self.value = int(self.value) - except (ValueError): - raise ValidationError(_('Must be an integer value')) - - if validator is not None: - self.run_validator(validator) - - def run_validator(self, validator): - """ - Run a validator against the 'value' field for this InvenTreeSetting object. - """ - - if validator is None: - return - - value = self.value - - # Boolean validator - if self.is_bool(): - # Value must "look like" a boolean value - if InvenTree.helpers.is_bool(value): - # Coerce into either "True" or "False" - value = InvenTree.helpers.str2bool(value) - else: - raise ValidationError({ - 'value': _('Value must be a boolean value') - }) - - # Integer validator - if self.is_int(): - - try: - # Coerce into an integer value - value = int(value) - except (ValueError, TypeError): - raise ValidationError({ - 'value': _('Value must be an integer value'), - }) - - # If a list of validators is supplied, iterate through each one - if type(validator) in [list, tuple]: - for v in validator: - self.run_validator(v) - - if callable(validator): - # We can accept function validators with a single argument - validator(self.value) - - 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. - """ - - super().validate_unique(exclude) - - try: - setting = InvenTreeSetting.objects.exclude(id=self.id).filter(key__iexact=self.key) - if setting.exists(): - raise ValidationError({'key': _('Key string must be unique')}) - except InvenTreeSetting.DoesNotExist: - pass - - def choices(self): - """ - Return the available choices for this setting (or None if no choices are defined) - """ - - return InvenTreeSetting.get_setting_choices(self.key) - - def is_bool(self): - """ - Check if this setting is required to be a boolean value - """ - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if validator == bool: - return True - - if type(validator) in [list, tuple]: - for v in validator: - if v == bool: - return True - - def as_bool(self): - """ - Return the value of this setting converted to a boolean value. - - Warning: Only use on values where is_bool evaluates to true! - """ - - return InvenTree.helpers.str2bool(self.value) - - def is_int(self): - """ - Check if the setting is required to be an integer value: - """ - - validator = InvenTreeSetting.get_setting_validator(self.key) - - if validator == int: - return True - - if type(validator) in [list, tuple]: - for v in validator: - if v == int: - return True - - return False - - def as_int(self): - """ - Return the value of this setting converted to a boolean value. - - If an error occurs, return the default value - """ - - try: - value = int(self.value) - except (ValueError, TypeError): - value = self.default_value() - - return value - class PriceBreak(models.Model): """