Simple app to add configuration options to a Django project.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: added cache, and help text

+134 -29
+12
HISTORY.rst
··· 3 3 History 4 4 ------- 5 5 6 + 2.3.0 (2022-08-03) 7 + ++++++++++++++++++ 8 + 9 + * Feat: Added thread cache 10 + * Feat: Added help text for option 11 + * Feat: Added typing 12 + 13 + 2.2.1 (2022-06-17) 14 + ++++++++++++++++++ 15 + 16 + * Fix: Optional default_app_config 17 + 6 18 2.2.0 (2022-06-17) 7 19 ++++++++++++++++++ 8 20
+6 -3
options/__init__.py
··· 1 1 """Simple app to add configuration options to a Django project.""" 2 + import django 3 + 2 4 from options.constants import CONVERTER, FILE, FLOAT, INT, STR, TYPE_CHOICES 3 5 from options.helpers import get_option_model, get_user_option_model 4 - 5 - default_app_config = "options.apps.ConfigurationsConfig" 6 6 7 7 __all__ = [ 8 8 "get_option_model", ··· 14 14 "TYPE_CHOICES", 15 15 "CONVERTER", 16 16 ] 17 - __version__ = "2.1.2" 17 + __version__ = "2.3.0" 18 + 19 + if django.VERSION < (3, 2): 20 + default_app_config = "options.apps.ConfigurationsConfig"
+2 -2
options/admin.py
··· 7 7 class OptionAdmin(admin.ModelAdmin): 8 8 """Manage configuration options.""" 9 9 10 - list_display = ["public_name", "value", "is_public"] 10 + list_display = ["public_name", "value", "is_public", "help_text"] 11 11 list_filter = ["is_public"] 12 12 search_fields = ["public_name", "name"] 13 13 ··· 16 16 class UserOptionAdmin(admin.ModelAdmin): 17 17 """Manage configuration user options.""" 18 18 19 - list_display = ["user", "public_name", "value", "is_public"] 19 + list_display = ["user", "public_name", "value", "is_public", "help_text"] 20 20 list_filter = ["is_public"] 21 21 search_fields = ["public_name", "name"]
+3 -1
options/constants.py
··· 1 + from typing import Dict, Type 2 + 1 3 from django.utils.translation import gettext_lazy as _ 2 4 3 5 FLOAT, INT, STR, FILE = (0, 1, 2, 3) ··· 7 9 (STR, _("String")), 8 10 (FILE, _("File")), 9 11 ) 10 - CONVERTER = {INT: int, FLOAT: float, STR: str, FILE: str} 12 + CONVERTER: Dict[int, Type] = {INT: int, FLOAT: float, STR: str, FILE: str}
+4 -3
options/helpers.py
··· 2 2 import os 3 3 import random 4 4 import time 5 + from typing import Type, Union 5 6 6 7 from django.apps import apps as django_apps 7 8 from django.core.exceptions import ImproperlyConfigured ··· 11 12 from options.constants import CONVERTER, FLOAT, INT, STR 12 13 13 14 14 - def get_option_model(): 15 + def get_option_model() -> Type: 15 16 """Return the Notification model that is active in this project.""" 16 17 from options.settings import DEFAULT_OPTION_MODEL 17 18 ··· 28 29 ) 29 30 30 31 31 - def get_user_option_model(): 32 + def get_user_option_model() -> Type: 32 33 """Return the Notification model that is active in this project.""" 33 34 from options.settings import DEFAULT_USER_OPTION_MODEL 34 35 ··· 46 47 ) 47 48 48 49 49 - def convert_value(value, value_type): 50 + def convert_value(value: str, value_type: int) -> Union[str, int, float]: 50 51 """Converts the given value to the given type.""" 51 52 default_values = {INT: 0, FLOAT: 1.0, STR: ""} 52 53 try:
+76 -15
options/managers.py
··· 1 + from threading import local 2 + from typing import TYPE_CHECKING, Optional, Sequence, Union 3 + 1 4 from django.db import models 2 5 3 6 from options import get_option_model 4 7 from options.settings import DEFAULT_EXCLUDE_USER 5 8 9 + if TYPE_CHECKING: 10 + from django.contrib.auth.models import User 11 + 12 + _active = local() # Active thread 13 + 6 14 7 15 class OptionQuerySet(models.QuerySet): 8 - def public(self): 16 + def public(self) -> "OptionQuerySet": 9 17 """Gets public options.""" 10 18 return self.filter(is_public=True) 11 19 ··· 13 21 class OptionManager(models.Manager): 14 22 """Manager for options.""" 15 23 16 - def get_queryset(self): 24 + cache_prefix = "_options_option_" 25 + 26 + def __get_cached_value( 27 + self, name: str 28 + ) -> Optional[Union[int, float, str, Sequence]]: 29 + return getattr(_active, f"{self.cache_prefix}{name}", None) 30 + 31 + def __set_cached_value( 32 + self, name: str, value: Union[int, float, str, Sequence] 33 + ) -> None: 34 + return setattr(_active, f"{self.cache_prefix}{name}", value) 35 + 36 + def get_queryset(self) -> "OptionQuerySet": 17 37 return OptionQuerySet(self.model, using=self._db) 18 38 19 - def public(self): 39 + def public(self) -> "OptionQuerySet": 20 40 """Gets public options.""" 21 41 return self.get_queryset().public() 22 42 23 - def get_value(self, name, default=None): 43 + def get_value( 44 + self, name: str, default: Optional[Union[int, float, str, Sequence]] = None 45 + ) -> Optional[Union[int, float, str, Sequence]]: 24 46 """Gets the value with the proper type.""" 47 + _cached_value = self.__get_cached_value(name=name) 48 + if _cached_value is not None: 49 + return _cached_value 25 50 try: 26 51 option = self.model.objects.get(name=name) 27 - return option.get_value() 52 + value = option.get_value() 28 53 except self.model.DoesNotExist: 29 - return default 54 + value = default 55 + self.__set_cached_value(name=name, value=value) 56 + return value 30 57 31 58 32 59 class UserOptionQuerySet(models.QuerySet): 33 - def public(self): 60 + def public(self) -> "UserOptionQuerySet": 34 61 """Gets public options.""" 35 62 return self.filter(is_public=True) 36 63 ··· 38 65 class UserOptionManager(models.Manager): 39 66 """Manager to handle user's custom options.""" 40 67 41 - def get_queryset(self): 68 + cache_prefix = "_option_user_option_" 69 + 70 + def __get_cached_value( 71 + self, name: str, user: Optional["User"] = None 72 + ) -> Optional[Union[int, float, str, Sequence]]: 73 + key = ( 74 + f"{self.cache_prefix}{name}" 75 + if user is None 76 + else f"{self.cache_prefix}{name}_{user.pk}" 77 + ) 78 + return getattr(_active, key, None) 79 + 80 + def __set_cached_value( 81 + self, 82 + name: str, 83 + value: Union[int, float, str, Sequence], 84 + user: Optional["User"] = None, 85 + ) -> None: 86 + key = ( 87 + f"{self.cache_prefix}{name}" 88 + if user is None 89 + else f"{self.cache_prefix}{name}_{user.pk}" 90 + ) 91 + return setattr(_active, key, value) 92 + 93 + def get_queryset(self) -> "UserOptionQuerySet": 42 94 return OptionQuerySet(self.model, using=self._db) 43 95 44 - def public(self): 96 + def public(self) -> "UserOptionQuerySet": 45 97 """Gets public options.""" 46 98 return self.get_queryset().public() 47 99 48 - def filter_user_customizable(self): 100 + def filter_user_customizable(self) -> "UserOptionQuerySet": 49 101 """Returns option that the user can customize himself.""" 50 102 return self.exclude(name__in=DEFAULT_EXCLUDE_USER) 51 103 52 - def get_value(self, name, user=None, default=None): 104 + def get_value( 105 + self, 106 + name, 107 + user: Optional["User"] = None, 108 + default: Optional[Union[int, float, str, Sequence]] = None, 109 + ) -> Optional[Union[int, float, str, Sequence]]: 53 110 """Gets the value with the proper type.""" 54 111 Option = get_option_model() 55 - 112 + _cached_value = self.__get_cached_value(name=name, user=user) 113 + if _cached_value is not None: 114 + return _cached_value 56 115 if user is None: 57 - return Option.objects.get_value(name=name, default=default) 116 + value = Option.objects.get_value(name=name, default=default) 58 117 try: 59 118 option = self.model.objects.get(user=user, name=name) 60 - return option.get_value() 119 + value = option.get_value() 61 120 except self.model.DoesNotExist: 62 - return Option.objects.get_value(name=name, default=default) 121 + value = Option.objects.get_value(name=name, default=default) 122 + self.__set_cached_value(name=name, value=value, user=user) 123 + return value
+23
options/migrations/0005_option_help_text_useroption_help_text.py
··· 1 + # Generated by Django 4.0.5 on 2022-08-03 11:11 2 + 3 + from django.db import migrations, models 4 + 5 + 6 + class Migration(migrations.Migration): 7 + 8 + dependencies = [ 9 + ("options", "0004_auto_20190829_0936"), 10 + ] 11 + 12 + operations = [ 13 + migrations.AddField( 14 + model_name="option", 15 + name="help_text", 16 + field=models.TextField(blank=True, null=True, verbose_name="help text"), 17 + ), 18 + migrations.AddField( 19 + model_name="useroption", 20 + name="help_text", 21 + field=models.TextField(blank=True, null=True, verbose_name="help text"), 22 + ), 23 + ]
+6 -3
options/models.py
··· 1 + from typing import Sequence, Union 2 + 1 3 from django.conf import settings 2 4 from django.core.exceptions import ValidationError 3 5 from django.db import models ··· 21 23 blank=True, 22 24 db_index=True, 23 25 ) 26 + help_text = models.TextField(verbose_name=_("help text"), blank=True, null=True) 24 27 type = models.PositiveIntegerField(choices=TYPE_CHOICES, default=STR) 25 28 value = models.CharField( 26 29 null=True, blank=True, default=None, max_length=256, verbose_name=_("value") ··· 34 37 class Meta: 35 38 abstract = True 36 39 37 - def __str__(self): 40 + def __str__(self) -> str: 38 41 return f"{self.public_name}" 39 42 40 - def get_value(self): 43 + def get_value(self) -> Union[int, str, float, Sequence]: 41 44 """Gets the value with the proper type. If the type is not 42 45 valid it would return the default value for the field, to avoid 43 46 problems with manual database modifications. ··· 51 54 values = self.value.split(",") 52 55 return [convert_value(value, self.type) for value in values] 53 56 54 - def clean(self): 57 + def clean(self) -> None: 55 58 """Calls to the converter to check the type conversion. Added exception 56 59 for lists, to check all values. 57 60 """
+1 -1
pyproject.toml
··· 1 1 [tool.poetry] 2 2 name = "django-simple-options" 3 - version = "2.2.0" 3 + version = "2.3.0" 4 4 description = "Simple app to add configuration options to a Django project." 5 5 readme = "README.rst" 6 6 authors = ["Marcos Gabarda <hey@marcosgabarda.com>"]
+1 -1
tests/test_options.py
··· 27 27 self.assertEqual("42", value) 28 28 29 29 def test_float_conversion_options(self): 30 - name = "string_option" 30 + name = "float_option" 31 31 OptionFactory(name=name, value="42.5", type=FLOAT) 32 32 value = Option.objects.get_value(name) 33 33 self.assertIsInstance(value, float)