diff --git a/constance/admin.py b/constance/admin.py index 58ba909..37c3cd3 100644 --- a/constance/admin.py +++ b/constance/admin.py @@ -15,7 +15,9 @@ from django.template.response import TemplateResponse from django.utils import six from django.utils.encoding import smart_bytes from django.utils.formats import localize +from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ +import django from . import LazyConfig, settings @@ -42,6 +44,25 @@ FIELDS = { float: (fields.FloatField, {'widget': NUMERIC_WIDGET}), } + +def parse_additional_fields(fields): + for key in fields: + field = fields[key] + + field[0] = import_string(field[0]) + + if 'widget' in field[1]: + klass = import_string(field[1]['widget']) + field[1]['widget'] = klass(**(field[1].get('widget_kwargs', {}) or {})) + + if 'widget_kwargs' in field[1]: + del field[1]['widget_kwargs'] + + return fields + + +FIELDS.update(parse_additional_fields(settings.ADDITIONAL_FIELDS)) + if not six.PY3: FIELDS.update({ long: INTEGER_LIKE, @@ -56,8 +77,13 @@ class ConstanceForm(forms.Form): super(ConstanceForm, self).__init__(*args, initial=initial, **kwargs) version_hash = hashlib.md5() - for name, (default, help_text) in settings.CONFIG.items(): - config_type = type(default) + for name, options in settings.CONFIG.items(): + default, help_text = options[0], options[1] + if len(options) == 3: + config_type = options[2] + else: + config_type = type(default) + if config_type not in FIELDS: raise ImproperlyConfigured(_("Constance doesn't support " "config values of the type " @@ -103,8 +129,8 @@ class ConstanceAdmin(admin.ModelAdmin): # First load a mapping between config name and default value if not self.has_change_permission(request, None): raise PermissionDenied - default_initial = ((name, default) - for name, (default, help_text) in settings.CONFIG.items()) + default_initial = ((name, options[0]) + for name, options in settings.CONFIG.items()) # Then update the mapping with actually values from the backend initial = dict(default_initial, **dict(config._backend.mget(settings.CONFIG.keys()))) @@ -128,7 +154,8 @@ class ConstanceAdmin(admin.ModelAdmin): 'form': form, 'media': self.media + form.media, } - for name, (default, help_text) in settings.CONFIG.items(): + for name, options in settings.CONFIG.items(): + default, help_text = options[0], options[1] # First try to load the value from the actual backend value = initial.get(name) # Then if the returned value is None, get the default diff --git a/constance/base.py b/constance/base.py index 99afe62..72812ad 100644 --- a/constance/base.py +++ b/constance/base.py @@ -11,7 +11,9 @@ class Config(object): def __getattr__(self, key): try: - default, help_text = settings.CONFIG[key] + if not len(settings.CONFIG[key]) in (2, 3): + raise AttributeError(key) + default = settings.CONFIG[key][0] except KeyError: raise AttributeError(key) result = self._backend.get(key) diff --git a/constance/settings.py b/constance/settings.py index 6f1350d..fd5edd4 100644 --- a/constance/settings.py +++ b/constance/settings.py @@ -5,6 +5,8 @@ BACKEND = getattr(settings, 'CONSTANCE_BACKEND', CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {}) +ADDITIONAL_FIELDS = getattr(settings, 'CONSTANCE_ADDITIONAL_FIELDS', {}) + DATABASE_CACHE_BACKEND = getattr(settings, 'CONSTANCE_DATABASE_CACHE_BACKEND', None) diff --git a/constance/templates/admin/constance/change_list.html b/constance/templates/admin/constance/change_list.html index 746e263..71c7da1 100644 --- a/constance/templates/admin/constance/change_list.html +++ b/constance/templates/admin/constance/change_list.html @@ -36,7 +36,7 @@ {% block bodyclass %}change-list{% endblock %} {% block content %} -
+
{% csrf_token %} {% if form.errors %} diff --git a/docs/index.rst b/docs/index.rst index a6ffd9c..9aae1b7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,6 +53,33 @@ admin will show. See the :ref:`Backends ` section how to setup the backend and finish the configuration. +Custom fields +------------- + +You can set the field type by the third value in the `CONSTANCE_CONFIG` +tuple. The value can be string or one of the supported types: + +.. code-block:: python + 'THE_ANSWER': (42, 'Answer to the Ultimate Question of Life, ' + 'The Universe, and Everything', str), + +If you can add your custom field types, you can use the +`CONSTANCE_ADDITIONAL_FIELDS` variable. Note that you must +use later evaluated strings instead of direct classes: + +.. code-block:: python + CONSTANCE_ADDITIONAL_FIELDS = { + 'yes_no_null_select': ['django.forms.fields.ChoiceField', + { + 'widget': 'django.forms.Select', + 'choices': (("-----", None), ("yes", "Yes"), ("no", "No")) + }], + } + + CONSTANCE_CONFIG = { + 'MY_SELECT_KEY': ('yes', 'select yes or no', 'yes_no_null_select'), + } + Usage ----- diff --git a/tests/settings.py b/tests/settings.py index 647a0e4..361a1b9 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -51,6 +51,14 @@ long_value = 123456 if not six.PY3: long_value = long(long_value) +CONSTANCE_ADDITIONAL_FIELDS = { + 'yes_no_null_select': ['django.forms.fields.ChoiceField', + { + 'widget': 'django.forms.Select', + 'choices': (("-----", None), ("yes", "Yes"), ("no", "No")) + }], + } + CONSTANCE_CONFIG = { 'INT_VALUE': (1, 'some int'), 'LONG_VALUE': (long_value, 'some looong int'), @@ -63,6 +71,7 @@ CONSTANCE_CONFIG = { 'FLOAT_VALUE': (3.1415926536, 'PI'), 'DATE_VALUE': (date(2010, 12, 24), 'Merry Chrismas'), 'TIME_VALUE': (time(23, 59, 59), 'And happy New Year'), + 'CHOICE_VALUE': ('yes', 'select yes or no', 'yes_no_null_select'), 'LINEBREAK_VALUE': ('Spam spam', 'eggs\neggs'), } diff --git a/tests/storage.py b/tests/storage.py index a78b02a..338daae 100644 --- a/tests/storage.py +++ b/tests/storage.py @@ -27,6 +27,7 @@ class StorageTestsMixin(object): self.assertEqual(self.config.FLOAT_VALUE, 3.1415926536) self.assertEqual(self.config.DATE_VALUE, date(2010, 12, 24)) self.assertEqual(self.config.TIME_VALUE, time(23, 59, 59)) + self.assertEqual(self.config.CHOICE_VALUE, 'yes') # set values self.config.INT_VALUE = 100 @@ -39,6 +40,7 @@ class StorageTestsMixin(object): self.config.FLOAT_VALUE = 2.718281845905 self.config.DATE_VALUE = date(2001, 12, 20) self.config.TIME_VALUE = time(1, 59, 0) + self.config.CHOICE_VALUE = 'no' # read again self.assertEqual(self.config.INT_VALUE, 100) @@ -51,6 +53,7 @@ class StorageTestsMixin(object): self.assertEqual(self.config.FLOAT_VALUE, 2.718281845905) self.assertEqual(self.config.DATE_VALUE, date(2001, 12, 20)) self.assertEqual(self.config.TIME_VALUE, time(1, 59, 0)) + self.assertEqual(self.config.CHOICE_VALUE, 'no') def test_nonexistent(self): try: