···33History
44-------
5566+2.0 (2019-08-27)
77++++++++++++++++++
88+99+* Drop support for Python 2.
1010+* ``Option`` model and ``UserOption`` model are now swappable.
1111+* Added option for file options.
1212+* Changed names of settings variables.
1313+* Added ``pyptoject.toml`` to distribute.
1414+* Tests running with pytest.
1515+6161.2 (2019-07-26)
717+++++++++++++++++
818
···11-import six
11+import hashlib
22+import os
33+import random
44+import time
2533-from options import INT, FLOAT, STRING, CONVERTER
66+from django.apps import apps as django_apps
77+from django.core.exceptions import ImproperlyConfigured
88+from django.utils.deconstruct import deconstructible
99+from django.utils.text import slugify
1010+1111+from options.constants import INT, FLOAT, STRING, CONVERTER
1212+from options.settings import DEFAULT_OPTION_MODEL, DEFAULT_USER_OPTION_MODEL
1313+1414+1515+def get_option_model():
1616+ """Return the Notification model that is active in this project."""
1717+ try:
1818+ return django_apps.get_model(DEFAULT_OPTION_MODEL, require_ready=False)
1919+ except ValueError:
2020+ raise ImproperlyConfigured(
2121+ "SIMPLE_OPTIONS_OPTION_MODEL must be of the form 'app_label.model_name'"
2222+ )
2323+ except LookupError:
2424+ raise ImproperlyConfigured(
2525+ "SIMPLE_OPTIONS_OPTION_MODEL refers to model '%s' that has not "
2626+ "been installed" % DEFAULT_OPTION_MODEL
2727+ )
2828+2929+3030+def get_user_option_model():
3131+ """Return the Notification model that is active in this project."""
3232+ try:
3333+ return django_apps.get_model(DEFAULT_USER_OPTION_MODEL, require_ready=False)
3434+ except ValueError:
3535+ raise ImproperlyConfigured(
3636+ "SIMPLE_OPTIONS_USER_OPTION_MODEL must be of the form "
3737+ "'app_label.model_name'"
3838+ )
3939+ except LookupError:
4040+ raise ImproperlyConfigured(
4141+ "SIMPLE_OPTIONS_USER_OPTION_MODEL refers to model '%s' that has not "
4242+ "been installed" % DEFAULT_OPTION_MODEL
4343+ )
444545646def convert_value(value, value_type):
747 """Converts the given value to the given type."""
848 default_values = {INT: 0, FLOAT: 1.0, STRING: ""}
949 try:
1010- option_value = CONVERTER.get(value_type, six.text_type)(value)
5050+ option_value = CONVERTER.get(value_type, str)(value)
1151 except ValueError:
1252 option_value = default_values.get(value_type)
1353 return option_value
5454+5555+5656+@deconstructible
5757+class UploadToDir(object):
5858+ """Generates a function to give to ``upload_to`` parameter in
5959+ models.Fields, that generates an name for uploaded files based on ``populate_from``
6060+ attribute.
6161+ """
6262+6363+ def __init__(self, path, populate_from=None, prefix=None, random_name=False):
6464+ self.path = path
6565+ self.populate_from = populate_from
6666+ self.random_name = random_name
6767+ self.prefix = prefix
6868+6969+ def __call__(self, instance, filename):
7070+ """Generates an name for an uploaded file."""
7171+ if self.populate_from is not None and not hasattr(instance, self.populate_from):
7272+ raise AttributeError(
7373+ "Instance hasn't {} attribute".format(self.populate_from)
7474+ )
7575+ ext = filename.split(".")[-1]
7676+ readable_name = slugify(filename.split(".")[0])
7777+ if self.populate_from:
7878+ readable_name = slugify(getattr(instance, self.populate_from))
7979+ if self.random_name:
8080+ random_name = hashlib.sha256(
8181+ "{}--{}".format(time.time(), random.random()).encode("utf-8")
8282+ )
8383+ readable_name = random_name.hexdigest()
8484+ elif self.prefix is not None:
8585+ readable_name = f"{self.prefix}{readable_name}"
8686+ file_name = "{}.{}".format(readable_name, ext)
8787+ return os.path.join(self.path, file_name)
+5-3
options/managers.py
···11from django.db import models
22-from options.settings import DEFAULT_EXCLUDE_USER_OPTIONS
22+33+from options import get_option_model
44+from options.settings import DEFAULT_EXCLUDE_USER
354657class OptionManager(models.Manager):
···19212022 def filter_user_customizable(self):
2123 """Returns option that the user can customize himself."""
2222- return self.exclude(name__in=DEFAULT_EXCLUDE_USER_OPTIONS)
2424+ return self.exclude(name__in=DEFAULT_EXCLUDE_USER)
23252426 def get_value(self, name, user=None, default=None):
2527 """Gets the value with the proper type."""
2626- from options.models import Option
2828+ Option = get_option_model()
27292830 if user is None:
2931 return Option.objects.get_value(name=name, default=default)
+20-12
options/models.py
···11-import six
21from django.conf import settings
32from django.core.exceptions import ValidationError
43from django.db import models
54from django.utils.translation import ugettext_lazy as _
6577-from options import STRING, TYPE_CHOICES, CONVERTER
88-from options.helpers import convert_value
66+from options.constants import STRING, TYPE_CHOICES, CONVERTER, FILE
77+from options.helpers import convert_value, UploadToDir
98from options.managers import OptionManager, UserOptionManager
1091110···1312 """Base model for system options and configurations."""
14131514 name = models.CharField(
1616- verbose_name=_("Parameter"), max_length=255, unique=True, db_index=True
1515+ verbose_name=_("parameter name"), max_length=255, unique=True, db_index=True
1716 )
1817 public_name = models.CharField(
1919- verbose_name=_("Public name of the parameter"),
1818+ verbose_name=_("public name of the parameter"),
2019 max_length=255,
2120 unique=False,
2221 db_index=True,
2322 )
2423 type = models.PositiveIntegerField(choices=TYPE_CHOICES, default=STRING)
2524 value = models.CharField(
2626- null=True, blank=True, default=None, max_length=256, verbose_name=_("Value")
2525+ null=True, blank=True, default=None, max_length=256, verbose_name=_("value")
2626+ )
2727+ file = models.FileField(
2828+ upload_to=UploadToDir("options", random_name=True), null=True, blank=True
2729 )
2830 is_list = models.BooleanField(default=False)
2931···3133 abstract = True
32343335 def __str__(self):
3434- return "%s" % self.public_name
3636+ return f"{self.public_name}"
35373638 def get_value(self):
3739 """Gets the value with the proper type. If the type is not
3840 valid it would return the default value for the field, to avoid
3939- problems with manual database modifications"""
4040-4141+ problems with manual database modifications.
4242+ """
4343+ # If the option is a file, returns the URL of the file
4444+ if self.type == FILE and self.file is not None:
4545+ return self.file.url
4146 if not self.is_list:
4247 return convert_value(self.value, self.type)
4348 else:
4449 values = self.value.split(",")
4545- return [convert_value(self.type, value) for value in values]
5050+ return [convert_value(type, value) for value in values]
46514752 def clean(self):
4853 """Calls to the converter to check the type conversion. Added exception
4949- for lists, to check all values."""
5454+ for lists, to check all values.
5555+ """
5056 try:
5157 values = [self.value] if not self.is_list else self.value.split(",")
5252- [CONVERTER.get(self.type, six.text_type)(value) for value in values]
5858+ [CONVERTER.get(self.type, str)(value) for value in values]
5359 except ValueError:
5460 raise ValidationError(_("Invalid value for this type."))
5561···65716672 class Meta:
6773 ordering = ["public_name"]
7474+ swappable = "SIMPLE_OPTIONS_OPTION_MODEL"
687569767077class UserOption(BaseOption):
···8087 class Meta:
8188 unique_together = ["user", "name"]
8289 ordering = ["public_name"]
9090+ swappable = "SIMPLE_OPTIONS_USER_OPTION_MODEL"
+7-4
options/rest_framework/serializers.py
···11from django.utils.translation import ugettext_lazy as _
22from rest_framework import serializers
3344-from options.settings import DEFAULT_EXCLUDE_USER_OPTIONS
55-from options.models import Option, UserOption
44+from options import get_option_model, get_user_option_model
55+from options.settings import DEFAULT_EXCLUDE_USER
66+77+Option = get_option_model()
88+UserOption = get_user_option_model()
69710811class OptionSerializer(serializers.ModelSerializer):
912 class Meta:
1013 model = Option
1111- fields = ["id", "name", "public_name", "type", "value", "is_list"]
1414+ fields = ["id", "name", "public_name", "type", "value", "file", "is_list"]
121513161417class UserOptionSerializer(OptionSerializer):
···17201821 def validate_name(self, value):
1922 """Checks if the name is in DEFAULT_EXCLUDE_USER_OPTIONS."""
2020- if value in DEFAULT_EXCLUDE_USER_OPTIONS:
2323+ if value in DEFAULT_EXCLUDE_USER:
2124 raise serializers.ValidationError(
2225 _("The name in the option can't be handle by the user.")
2326 )
···11from django.conf import settings
2233+# Needed to build and publish with Flit
44+# ------------------------------------------------------------------------------
55+SECRET_KEY = "snitch"
66+77+# Specific project configuration
88+# ------------------------------------------------------------------------------
39# Set on settings the default options for this project. These will be created
410# on the post migrate signal handler.
511# Sample:
612#
77-# CONFIGURATION_DEFAULT_OPTIONS = {
1313+# SIMPLE_OPTIONS_CONFIGURATION = {
814# "sold_out": {
915# "value": 0,
1016# "type": INT,
···1218# },
1319# }
1420#
1515-DEFAULT_OPTIONS = getattr(settings, "CONFIGURATION_DEFAULT_OPTIONS", {})
2121+DEFAULT_CONFIGURATION = getattr(settings, "SIMPLE_OPTIONS_CONFIGURATION", {})
16221723# Set the list of options that the user can't customize.
1818-DEFAULT_EXCLUDE_USER_OPTIONS = getattr(settings, "EXCLUDE_USER_OPTIONS", tuple())
2424+DEFAULT_EXCLUDE_USER = getattr(settings, "SIMPLE_OPTIONS_EXCLUDE_USER", tuple())
2525+2626+# Swappable Option model
2727+DEFAULT_OPTION_MODEL = getattr(
2828+ settings, "SIMPLE_OPTIONS_OPTION_MODEL", "options.Option"
2929+)
3030+3131+# Swappable UserOption model
3232+DEFAULT_USER_OPTION_MODEL = getattr(
3333+ settings, "SIMPLE_OPTIONS_USER_OPTION_MODEL", "options.UserOption"
3434+)
···11-from tests import settings as _settings
22-33-TEST_SETTINGS = dict(
44- (k, getattr(_settings, k)) for k in dir(_settings) if k == k.upper()
55-)