fabiocaccamo/django-extra-settings

View on GitHub
extra_settings/models.py

Summary

Maintainability
A
3 hrs
Test Coverage
from decimal import Decimal

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
from jsonfield import JSONField

from extra_settings import fields
from extra_settings.cache import get_cached_setting, set_cached_setting
from extra_settings.utils import enforce_uppercase_setting, import_function


class Setting(models.Model):
    """
    This class describes a Setting model.
    """

    @staticmethod
    def _cache_all_settings():
        settings_qs = Setting.objects.all()
        settings_list = list(settings_qs)
        for setting_obj in settings_list:
            setting_name = setting_obj.name
            setting_value = setting_obj.value
            set_cached_setting(setting_name, setting_value)

    @staticmethod
    def _get_from_cache(name):
        return get_cached_setting(name)

    @staticmethod
    def _get_from_database(name):
        try:
            setting_obj = Setting.objects.get(name=name)
            value = setting_obj.value
            set_cached_setting(name, value)
            return value
        except Setting.DoesNotExist:
            return None

    @staticmethod
    def _get_from_settings(name):
        if settings.EXTRA_SETTINGS_FALLBACK_TO_CONF_SETTINGS:
            return getattr(settings, name, None)
        return None

    @classmethod
    def get(cls, name, default=None):
        val = cls._get_from_cache(name)
        if val is None:
            val = cls._get_from_database(name)
            if val is None:
                val = cls._get_from_settings(name)
                if val is None:
                    val = default
            else:
                # value found, it means that cache was not set or expired.
                # cache all other settings to avoid multiple queries
                # in pages using the template more than once.
                cls._cache_all_settings()
        if val is not None:
            set_cached_setting(name, val)
        return val

    @classmethod
    def set_defaults(cls, defaults):
        if not isinstance(defaults, list):
            raise ValueError("Setting 'defaults' must be a list of dicts items.")
        if not defaults:
            return
        for item in defaults:
            if not isinstance(item, dict):
                raise ValueError("Setting 'defaults' item must be a dict.")
            try:
                name = item["name"]
                value_type = item["type"]
                value_type = value_type.replace("Setting.TYPE_", "").lower()
                value = item["value"]
            except KeyError as error:
                raise ValueError(
                    "Setting 'defaults' item must contain "
                    "'name', 'type' and 'value' keys."
                ) from error
            validator = item.get("validator", None)
            description = item.get("description", "")
            setting_obj, setting_created = cls.objects.get_or_create(
                name=name,
                defaults={
                    "value_type": value_type,
                    "description": description,
                    "validator": validator,
                },
            )
            if setting_created:
                setting_obj.value = value
                setting_obj.save()

    @classmethod
    def set_defaults_from_settings(cls, *args, **kwargs):
        cls.set_defaults(settings.EXTRA_SETTINGS_DEFAULTS)

    TYPE_BOOL = "bool"
    # TYPE_COLOR = "color" # TODO
    TYPE_DATE = "date"
    TYPE_DATETIME = "datetime"
    TYPE_DECIMAL = "decimal"
    TYPE_DURATION = "duration"
    TYPE_EMAIL = "email"
    TYPE_FILE = "file"
    TYPE_FLOAT = "float"
    # TYPE_HTML = "html"
    TYPE_IMAGE = "image"
    TYPE_INT = "int"
    TYPE_JSON = "json"
    TYPE_STRING = "string"
    TYPE_TEXT = "text"
    TYPE_TIME = "time"
    # TYPE_UUID = "uuid" # TODO
    TYPE_URL = "url"

    TYPE_CHOICES = (
        (TYPE_BOOL, TYPE_BOOL),
        # (TYPE_COLOR, TYPE_COLOR, ),
        (TYPE_DATE, TYPE_DATE),
        (TYPE_DATETIME, TYPE_DATETIME),
        (TYPE_DECIMAL, TYPE_DECIMAL),
        (TYPE_DURATION, TYPE_DURATION),
        (TYPE_EMAIL, TYPE_EMAIL),
        (TYPE_FILE, TYPE_FILE),
        (TYPE_FLOAT, TYPE_FLOAT),
        # (TYPE_HTML, TYPE_HTML, ),
        (TYPE_IMAGE, TYPE_IMAGE),
        (TYPE_INT, TYPE_INT),
        (TYPE_JSON, TYPE_JSON),
        (TYPE_STRING, TYPE_STRING),
        (TYPE_TEXT, TYPE_TEXT),
        (TYPE_TIME, TYPE_TIME),
        # (TYPE_UUID, TYPE_UUID, ),
        (TYPE_URL, TYPE_URL),
    )

    name = models.CharField(
        max_length=255,
        unique=True,
        verbose_name=_("Name"),
        help_text=_("(e.g. SETTING_NAME)"),
    )
    value_type = models.CharField(
        max_length=20,
        choices=TYPE_CHOICES,
        verbose_name=_(
            "Type",
        ),
    )
    description = models.TextField(
        blank=True,
        null=True,
        verbose_name=_("Description"),
    )

    value_bool = models.BooleanField(
        default=False,
        verbose_name=_("Value"),
    )
    value_date = models.DateField(
        blank=True,
        null=True,
        verbose_name=_("Value"),
    )
    value_datetime = models.DateTimeField(
        blank=True,
        null=True,
        verbose_name=_("Value"),
    )
    value_decimal = models.DecimalField(
        blank=True,
        max_digits=19,
        decimal_places=10,
        default=Decimal("0.0"),
        verbose_name=_("Value"),
    )
    value_duration = models.DurationField(
        blank=True,
        null=True,
        verbose_name=_("Value"),
    )
    value_email = models.EmailField(
        blank=True,
        verbose_name=_("Value"),
    )
    value_file = models.FileField(
        blank=True,
        upload_to=fields.upload_to_files,
        verbose_name=_("Value"),
    )
    value_float = models.FloatField(
        blank=True,
        default=0.0,
        verbose_name=_("Value"),
    )
    value_image = models.FileField(
        blank=True,
        upload_to=fields.upload_to_images,
        verbose_name=_("Value"),
    )
    value_int = models.IntegerField(
        blank=True,
        default=0,
        verbose_name=_("Value"),
    )
    value_json = JSONField(
        blank=True,
        default=dict,
        verbose_name=_("Value"),
    )
    value_string = models.CharField(
        blank=True,
        max_length=255,
        verbose_name=_("Value"),
    )
    value_text = models.TextField(
        blank=True,
        verbose_name=_("Value"),
    )
    value_time = models.TimeField(
        blank=True,
        null=True,
        verbose_name=_("Value"),
    )
    value_url = models.URLField(
        blank=True,
        verbose_name=_("Value"),
    )
    validator = models.CharField(
        blank=True,
        null=True,
        max_length=255,
        verbose_name=_("Validator"),
        help_text=_(
            "Full python path to a validator function, "
            "eg. 'myapp.mymodule.my_validator'"
        ),
    )

    @property
    def value_field_name(self):
        return f"value_{self.value_type}"

    @property
    def value(self):
        return getattr(self, self.value_field_name, None)

    @value.setter
    def value(self, new_value):
        setattr(self, self.value_field_name, new_value)

    def __init__(self, *args, **kwargs):
        value = kwargs.pop("value", None)
        super().__init__(*args, **kwargs)
        if value is not None:
            self.value = value
        self.name_initial = self.name

    def validate(self):
        if not self.validator:
            return
        validator_func = import_function(self.validator)
        if not validator_func:
            raise ValueError(f"Invalid validator function path: {self.validator!r}")
        is_valid = validator_func(self.value)
        if not is_valid:
            raise ValidationError(
                f"Invalid value for setting {self.name!r}, "
                f"{self.value!r} is not validated by {self.validator!r} validator."
            )

    def save(self, *args, **kwargs):
        if settings.EXTRA_SETTINGS_ENFORCE_UPPERCASE_SETTINGS:
            self.name = enforce_uppercase_setting(self.name)
        super().save(*args, **kwargs)

    def clean(self):
        super().clean()
        self.validate()

    class Meta:
        ordering = ["name"]
        verbose_name = _("Setting")
        verbose_name_plural = _("Settings")

    def __str__(self):
        return force_str(f"{self.name} [{self.value_type}]")