From eea0d2fdf9c6e1ece0e12be7abd1d669572136ba Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 1 Dec 2010 00:20:23 +0100 Subject: [PATCH 01/14] Abstracted constance backends to and added a cached database backend. Moved around the settings code and simplified the module loading. --- constance/admin.py | 11 +-- constance/backends.py | 84 +++++++++++++++++++ constance/config.py | 40 +++------ constance/models.py | 25 ++++++ constance/settings.py | 17 ++++ constance/utils.py | 5 ++ .../testproject/test_app/tests/test_config.py | 28 +++++-- 7 files changed, 170 insertions(+), 40 deletions(-) create mode 100644 constance/backends.py create mode 100644 constance/settings.py create mode 100644 constance/utils.py diff --git a/constance/admin.py b/constance/admin.py index f318d35..b9ca7ac 100644 --- a/constance/admin.py +++ b/constance/admin.py @@ -6,18 +6,15 @@ from django import forms from django.contrib import admin from django.contrib.admin import widgets from django.contrib.admin.options import csrf_protect_m -from django.conf import settings from django.conf.urls.defaults import patterns, url -from django.core.urlresolvers import reverse from django.forms import fields from django.http import HttpResponseRedirect from django.shortcuts import render_to_response from django.template.context import RequestContext -from django.utils.functional import update_wrapper from django.utils.formats import localize from django.utils.translation import ugettext_lazy as _ -from constance import config +from constance import config, settings NUMERIC_WIDGET = forms.TextInput(attrs={'size': 10}) @@ -42,7 +39,7 @@ FIELDS = { class ConstanceForm(forms.Form): def __init__(self, *args, **kwargs): super(ConstanceForm, self).__init__(*args, **kwargs) - for name, (default, help_text) in settings.CONSTANCE_CONFIG.items(): + for name, (default, help_text) in settings.CONFIG.items(): field_class, kwargs = FIELDS[type(default)] self.fields[name] = field_class(label=name, **kwargs) @@ -65,7 +62,7 @@ class ConstanceAdmin(admin.ModelAdmin): @csrf_protect_m def changelist_view(self, request, extra_context=None): form = ConstanceForm(initial=dict((name, getattr(config, name)) - for name in settings.CONSTANCE_CONFIG)) + for name in settings.CONFIG)) if request.method == 'POST': form = ConstanceForm(request.POST) if form.is_valid(): @@ -81,7 +78,7 @@ class ConstanceAdmin(admin.ModelAdmin): 'form': form, 'media': self.media + form.media, } - for name, (default, help_text) in settings.CONSTANCE_CONFIG.iteritems(): + for name, (default, help_text) in settings.CONFIG.iteritems(): value = getattr(config, name) context['config'].append({ 'name': name, diff --git a/constance/backends.py b/constance/backends.py new file mode 100644 index 0000000..4ff915c --- /dev/null +++ b/constance/backends.py @@ -0,0 +1,84 @@ +from django.core.exceptions import ImproperlyConfigured +from django.db.models.signals import post_save +from django.utils.functional import memoize + +from constance import settings +from constance.utils import import_module_attr + +try: + from cPickle import loads, dumps +except ImportError: + from pickle import loads, dumps + + +class Backend(object): + + def __init__(self, prefix): + self._prefix = prefix + + def get(self, key): + """ + Get the key from the backend store and return it. + Return None if not found. + """ + raise NotImplementedError + + def set(self, key, value): + """ + Add the value to the backend store given the key. + """ + raise NotImplementedError + +db_cache = {} + +class DatabaseBackend(Backend): + + def __init__(self, prefix): + super(DatabaseBackend, self).__init__(prefix) + from constance.models import Constance + if not Constance._meta.installed: + raise ImproperlyConfigured( + "The constance app isn't installed correctly. " + "Make sure it's listed in the INSTALLED_APPS setting.") + self._model = Constance + # Clear simple cache. + post_save.connect(self.clear, sender=self._model) + + def _get(self, key): + try: + value = self._model._default_manager.get(key=key).value + except self._model.DoesNotExist: + return None + return value + get = memoize(_get, db_cache, 2) + + def set(self, key, value): + constance, created = self._model._default_manager.get_or_create(key=key, defaults={'value': value}) + if not created: + constance.value = value + constance.save() + + def clear(self, sender, instance, created, **kwargs): + if not created: + db_cache.clear() + +class RedisBackend(Backend): + + def __init__(self, prefix): + super(RedisBackend, self).__init__(prefix) + connection_cls = settings.CONNECTION_CLASS + if connection_cls is not None: + self._rd = import_module_attr(connection_cls)() + else: + import redis + self._rd = redis.Redis(**settings.REDIS_CONNECTION) + + def get(self, key): + value = self._rd.get("%s%s" % (self._prefix, key)) + if value: + return loads(value) + return None + + def set(self, key, value): + self._rd.set("%s%s" % (self._prefix, key), dumps(value)) + diff --git a/constance/config.py b/constance/config.py index 0bf670a..24860ab 100644 --- a/constance/config.py +++ b/constance/config.py @@ -1,44 +1,30 @@ -import os -import redis - -try: - from cPickle import loads, dumps -except ImportError: - from pickle import loads, dumps - -def import_module(path): - package, module = path.rsplit('.', 1) - return getattr(__import__(package, None, None, [module]), module) - -settings = import_module(os.getenv('CONSTANCE_SETTINGS_MODULE', 'django.conf.settings')) - +from constance import settings +from constance.utils import import_module_attr class Config(object): - + """ + The global config wrapper that handles the backend. + """ def __init__(self): - super(Config, self).__setattr__('_prefix', getattr(settings, 'CONSTANCE_PREFIX', 'constance:')) - try: - super(Config, self).__setattr__('_rd', import_module(settings.CONSTANCE_CONNECTION_CLASS)()) - except AttributeError: - super(Config, self).__setattr__('_rd', redis.Redis(**settings.CONSTANCE_CONNECTION)) + super(Config, self).__setattr__( + '_backend', import_module_attr(settings.BACKEND)(settings.PREFIX)) def __getattr__(self, key): try: - default, help_text = settings.CONSTANCE_CONFIG[key] + default, help_text = settings.CONFIG[key] except KeyError, e: raise AttributeError(key) - result = self._rd.get("%s%s" % (self._prefix, key)) + result = self._backend.get(key) if result is None: result = default setattr(self, key, default) return result - return loads(result) + return result def __setattr__(self, key, value): - if key not in settings.CONSTANCE_CONFIG: + if key not in settings.CONFIG: raise AttributeError(key) - self._rd.set("%s%s" % (self._prefix, key), dumps(value)) + self._backend.set(key, value) def __dir__(self): - return settings.CONSTANCE_CONFIG.keys() - + return settings.CONFIG.iterkeys() diff --git a/constance/models.py b/constance/models.py index e69de29..e4de4ed 100644 --- a/constance/models.py +++ b/constance/models.py @@ -0,0 +1,25 @@ +from constance import settings + +if settings.BACKEND == 'constance.backends.DatabaseBackend': + + from django.db import models + from django.core.exceptions import ImproperlyConfigured + + from django.utils.translation import ugettext_lazy as _ + + try: + from picklefield import PickledObjectField + except ImportError: + raise ImproperlyConfigured("Couldn't find the the 3rd party app " + "django-picklefield which is required for " + "the constance database backend.") + + class Constance(models.Model): + key = models.TextField() + value = PickledObjectField() + + class Meta: + verbose_name = _('constance') + verbose_name_plural = _('constances') + db_table = 'constance_config' + diff --git a/constance/settings.py b/constance/settings.py new file mode 100644 index 0000000..6756abc --- /dev/null +++ b/constance/settings.py @@ -0,0 +1,17 @@ +import os +from constance.utils import import_module_attr + +settings = import_module_attr( + os.getenv('CONSTANCE_SETTINGS_MODULE', 'django.conf.settings') +) + +PREFIX = getattr(settings, 'CONSTANCE_PREFIX', 'constance:') + +BACKEND = getattr(settings, 'CONSTANCE_BACKEND', 'constance.backends.DatabaseBackend') + +CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {}) + +CONNECTION_CLASS = getattr(settings, 'CONSTANCE_CONNECTION_CLASS', None) + +REDIS_CONNECTION = getattr(settings, 'CONSTANCE_REDIS_CONNECTION', + getattr(settings, 'CONSTANCE_CONNECTION', {})) diff --git a/constance/utils.py b/constance/utils.py new file mode 100644 index 0000000..f6dc970 --- /dev/null +++ b/constance/utils.py @@ -0,0 +1,5 @@ +from django.utils.importlib import import_module + +def import_module_attr(path): + package, module = path.rsplit('.', 1) + return getattr(import_module(package), module) diff --git a/tests/testproject/test_app/tests/test_config.py b/tests/testproject/test_app/tests/test_config.py index 1ca6756..0b2cc47 100644 --- a/tests/testproject/test_app/tests/test_config.py +++ b/tests/testproject/test_app/tests/test_config.py @@ -1,5 +1,6 @@ # -*- encoding: utf-8 -*- +import sys from datetime import datetime, date, time from decimal import Decimal @@ -8,21 +9,19 @@ from django.conf import settings from django.contrib import admin from django.contrib.auth.models import User -from constance import config +from constance import settings from constance.admin import Config # Use django RequestFactory later on from testproject.test_app.tests.helpers import FakeRequest - -class TestStorage(TestCase): - - def tearDown(self): - config._rd.clear() +class TestStorage(object): def test_store(self): # read defaults + del sys.modules['constance'] + from constance import config self.assertEquals(config.INT_VALUE, 1) self.assertEquals(config.LONG_VALUE, 123456L) self.assertEquals(config.BOOL_VALUE, True) @@ -59,6 +58,7 @@ class TestStorage(TestCase): self.assertEquals(config.TIME_VALUE, time(1, 59, 0)) def test_nonexistent(self): + from constance import config try: config.NON_EXISTENT except Exception, e: @@ -71,6 +71,22 @@ class TestStorage(TestCase): pass self.assertEquals(type(e), AttributeError) +class TestRedis(TestCase, TestStorage): + + def setUp(self): + self.old_backend = settings.BACKEND + settings.BACKEND = 'constance.backends.RedisBackend' + + def tearDown(self): + del sys.modules['constance'] + from constance import config + config._backend._rd.clear() + settings.BACKEND = self.old_backend + import constance + constance.config = Config() + +class TestDatabase(TestCase, TestStorage): + pass class TestAdmin(TestCase): model = Config From 82e19cec9f821cae27a2eaf64ccd2912c165da52 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 1 Dec 2010 18:13:37 +0100 Subject: [PATCH 02/14] Refactored backend system slightly to have a nicer directory structure. Using the database backend now only requires to add 'constance.backends.database' to INSTALLED_APPS and setting CONSTANCE_BACKEND to 'constance.backends.database.DatabaseBackend'. Also reverted default backend to being the Redis backend. --- constance/backends.py | 84 ------------------- constance/backends/__init__.py | 16 ++++ constance/backends/database/__init__.py | 37 ++++++++ constance/backends/database/models.py | 20 +++++ constance/backends/redis.py | 29 +++++++ constance/config.py | 9 +- constance/models.py | 25 ------ constance/settings.py | 8 +- tests/testproject/settings.py | 1 + .../testproject/test_app/tests/test_config.py | 13 ++- 10 files changed, 123 insertions(+), 119 deletions(-) create mode 100644 constance/backends/__init__.py create mode 100644 constance/backends/database/__init__.py create mode 100644 constance/backends/database/models.py create mode 100644 constance/backends/redis.py delete mode 100644 constance/models.py diff --git a/constance/backends.py b/constance/backends.py index 4ff915c..e69de29 100644 --- a/constance/backends.py +++ b/constance/backends.py @@ -1,84 +0,0 @@ -from django.core.exceptions import ImproperlyConfigured -from django.db.models.signals import post_save -from django.utils.functional import memoize - -from constance import settings -from constance.utils import import_module_attr - -try: - from cPickle import loads, dumps -except ImportError: - from pickle import loads, dumps - - -class Backend(object): - - def __init__(self, prefix): - self._prefix = prefix - - def get(self, key): - """ - Get the key from the backend store and return it. - Return None if not found. - """ - raise NotImplementedError - - def set(self, key, value): - """ - Add the value to the backend store given the key. - """ - raise NotImplementedError - -db_cache = {} - -class DatabaseBackend(Backend): - - def __init__(self, prefix): - super(DatabaseBackend, self).__init__(prefix) - from constance.models import Constance - if not Constance._meta.installed: - raise ImproperlyConfigured( - "The constance app isn't installed correctly. " - "Make sure it's listed in the INSTALLED_APPS setting.") - self._model = Constance - # Clear simple cache. - post_save.connect(self.clear, sender=self._model) - - def _get(self, key): - try: - value = self._model._default_manager.get(key=key).value - except self._model.DoesNotExist: - return None - return value - get = memoize(_get, db_cache, 2) - - def set(self, key, value): - constance, created = self._model._default_manager.get_or_create(key=key, defaults={'value': value}) - if not created: - constance.value = value - constance.save() - - def clear(self, sender, instance, created, **kwargs): - if not created: - db_cache.clear() - -class RedisBackend(Backend): - - def __init__(self, prefix): - super(RedisBackend, self).__init__(prefix) - connection_cls = settings.CONNECTION_CLASS - if connection_cls is not None: - self._rd = import_module_attr(connection_cls)() - else: - import redis - self._rd = redis.Redis(**settings.REDIS_CONNECTION) - - def get(self, key): - value = self._rd.get("%s%s" % (self._prefix, key)) - if value: - return loads(value) - return None - - def set(self, key, value): - self._rd.set("%s%s" % (self._prefix, key), dumps(value)) - diff --git a/constance/backends/__init__.py b/constance/backends/__init__.py new file mode 100644 index 0000000..1eb0a02 --- /dev/null +++ b/constance/backends/__init__.py @@ -0,0 +1,16 @@ + +class Backend(object): + + def get(self, key): + """ + Get the key from the backend store and return it. + Return None if not found. + """ + raise NotImplementedError + + def set(self, key, value): + """ + Add the value to the backend store given the key. + """ + raise NotImplementedError + diff --git a/constance/backends/database/__init__.py b/constance/backends/database/__init__.py new file mode 100644 index 0000000..2563923 --- /dev/null +++ b/constance/backends/database/__init__.py @@ -0,0 +1,37 @@ +from django.core.exceptions import ImproperlyConfigured +from django.db.models.signals import post_save +from django.utils.functional import memoize + +from constance.backends import Backend + +db_cache = {} + +class DatabaseBackend(Backend): + + def __init__(self): + from constance.backends.database.models import Constance + if not Constance._meta.installed: + raise ImproperlyConfigured( + "The constance.contrib.database app isn't installed correctly. " + "Make sure it's listed in the INSTALLED_APPS setting.") + self._model = Constance + # Clear simple cache. + post_save.connect(self.clear, sender=self._model) + + def _get(self, key): + try: + value = self._model._default_manager.get(key=key).value + except self._model.DoesNotExist: + return None + return value + get = memoize(_get, db_cache, 2) + + def set(self, key, value): + constance, created = self._model._default_manager.get_or_create(key=key, defaults={'value': value}) + if not created: + constance.value = value + constance.save() + + def clear(self, sender, instance, created, **kwargs): + if not created: + db_cache.clear() diff --git a/constance/backends/database/models.py b/constance/backends/database/models.py new file mode 100644 index 0000000..f5bc3c6 --- /dev/null +++ b/constance/backends/database/models.py @@ -0,0 +1,20 @@ +from django.db import models +from django.core.exceptions import ImproperlyConfigured + +from django.utils.translation import ugettext_lazy as _ + +try: + from picklefield import PickledObjectField +except ImportError: + raise ImproperlyConfigured("Couldn't find the the 3rd party app " + "django-picklefield which is required for " + "the constance database backend.") + +class Constance(models.Model): + key = models.TextField() + value = PickledObjectField() + + class Meta: + verbose_name = _('constance') + verbose_name_plural = _('constances') + db_table = 'constance_config' diff --git a/constance/backends/redis.py b/constance/backends/redis.py new file mode 100644 index 0000000..8b1a586 --- /dev/null +++ b/constance/backends/redis.py @@ -0,0 +1,29 @@ +from constance import settings, utils +from constance.backends import Backend + +try: + from cPickle import loads, dumps +except ImportError: + from pickle import loads, dumps + + +class RedisBackend(Backend): + + def __init__(self): + super(RedisBackend, self).__init__() + self._prefix = settings.PREFIX + connection_cls = settings.CONNECTION_CLASS + if connection_cls is not None: + self._rd = utils.import_module_attr(connection_cls)() + else: + import redis + self._rd = redis.Redis(**settings.REDIS_CONNECTION) + + def get(self, key): + value = self._rd.get("%s%s" % (self._prefix, key)) + if value: + return loads(value) + return None + + def set(self, key, value): + self._rd.set("%s%s" % (self._prefix, key), dumps(value)) diff --git a/constance/config.py b/constance/config.py index 24860ab..79751db 100644 --- a/constance/config.py +++ b/constance/config.py @@ -1,18 +1,17 @@ -from constance import settings -from constance.utils import import_module_attr +from constance import settings, utils class Config(object): """ The global config wrapper that handles the backend. """ def __init__(self): - super(Config, self).__setattr__( - '_backend', import_module_attr(settings.BACKEND)(settings.PREFIX)) + super(Config, self).__setattr__('_backend', + utils.import_module_attr(settings.BACKEND)()) def __getattr__(self, key): try: default, help_text = settings.CONFIG[key] - except KeyError, e: + except KeyError: raise AttributeError(key) result = self._backend.get(key) if result is None: diff --git a/constance/models.py b/constance/models.py deleted file mode 100644 index e4de4ed..0000000 --- a/constance/models.py +++ /dev/null @@ -1,25 +0,0 @@ -from constance import settings - -if settings.BACKEND == 'constance.backends.DatabaseBackend': - - from django.db import models - from django.core.exceptions import ImproperlyConfigured - - from django.utils.translation import ugettext_lazy as _ - - try: - from picklefield import PickledObjectField - except ImportError: - raise ImproperlyConfigured("Couldn't find the the 3rd party app " - "django-picklefield which is required for " - "the constance database backend.") - - class Constance(models.Model): - key = models.TextField() - value = PickledObjectField() - - class Meta: - verbose_name = _('constance') - verbose_name_plural = _('constances') - db_table = 'constance_config' - diff --git a/constance/settings.py b/constance/settings.py index 6756abc..a5948e1 100644 --- a/constance/settings.py +++ b/constance/settings.py @@ -5,13 +5,15 @@ settings = import_module_attr( os.getenv('CONSTANCE_SETTINGS_MODULE', 'django.conf.settings') ) -PREFIX = getattr(settings, 'CONSTANCE_PREFIX', 'constance:') +PREFIX = getattr(settings, 'CONSTANCE_REDIS_PREFIX', + getattr(settings, 'CONSTANCE_PREFIX', 'constance:')) -BACKEND = getattr(settings, 'CONSTANCE_BACKEND', 'constance.backends.DatabaseBackend') +BACKEND = getattr(settings, 'CONSTANCE_BACKEND', 'constance.backends.redis.RedisBackend') CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {}) -CONNECTION_CLASS = getattr(settings, 'CONSTANCE_CONNECTION_CLASS', None) +CONNECTION_CLASS = getattr(settings, 'CONSTANCE_REDIS_CONNECTION_CLASS', + getattr(settings, 'CONSTANCE_CONNECTION_CLASS', None)) REDIS_CONNECTION = getattr(settings, 'CONSTANCE_REDIS_CONNECTION', getattr(settings, 'CONSTANCE_CONNECTION', {})) diff --git a/tests/testproject/settings.py b/tests/testproject/settings.py index 89163b5..5d8efc6 100644 --- a/tests/testproject/settings.py +++ b/tests/testproject/settings.py @@ -16,6 +16,7 @@ INSTALLED_APPS = ( 'django.contrib.admin', 'constance', + 'constance.backends.database', 'testproject.test_app', ) diff --git a/tests/testproject/test_app/tests/test_config.py b/tests/testproject/test_app/tests/test_config.py index 0b2cc47..cb3675d 100644 --- a/tests/testproject/test_app/tests/test_config.py +++ b/tests/testproject/test_app/tests/test_config.py @@ -75,7 +75,7 @@ class TestRedis(TestCase, TestStorage): def setUp(self): self.old_backend = settings.BACKEND - settings.BACKEND = 'constance.backends.RedisBackend' + settings.BACKEND = 'constance.backends.redis.RedisBackend' def tearDown(self): del sys.modules['constance'] @@ -86,7 +86,16 @@ class TestRedis(TestCase, TestStorage): constance.config = Config() class TestDatabase(TestCase, TestStorage): - pass + + def setUp(self): + self.old_backend = settings.BACKEND + settings.BACKEND = 'constance.backends.database.DatabaseBackend' + + def tearDown(self): + del sys.modules['constance'] + settings.BACKEND = self.old_backend + import constance + constance.config = Config() class TestAdmin(TestCase): model = Config From 028dd17b938cc9fce8cf5bfcef8dbc29a7f11649 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 1 Dec 2010 18:16:27 +0100 Subject: [PATCH 03/14] Raise an ImproperlyConfigured exception if the Redis backend is used (default) and redis-py can't be found. --- constance/backends/redis.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/constance/backends/redis.py b/constance/backends/redis.py index 8b1a586..ca982de 100644 --- a/constance/backends/redis.py +++ b/constance/backends/redis.py @@ -1,3 +1,5 @@ +from django.core.exceptions import ImproperlyConfigured + from constance import settings, utils from constance.backends import Backend @@ -16,7 +18,11 @@ class RedisBackend(Backend): if connection_cls is not None: self._rd = utils.import_module_attr(connection_cls)() else: - import redis + try: + import redis + except ImportError: + raise ImproperlyConfigured( + "The Redis backend requires redis-py to be installed.") self._rd = redis.Redis(**settings.REDIS_CONNECTION) def get(self, key): From fa7fab6dd18e3d5512d3ca8eae9bf87268adf2b6 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 1 Dec 2010 19:29:10 +0100 Subject: [PATCH 04/14] Update README extensively with new and old features. --- README.rst | 122 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index 84472bd..f8b833e 100644 --- a/README.rst +++ b/README.rst @@ -1,38 +1,37 @@ -Dynamic Django settings in Redis. +Dynamic Django settings +======================= Features -======== +-------- * Easy migrate your static settings to dynamic settings. * Admin interface to edit the dynamic settings. Installation -============ +------------ -Install from here using ``pip``:: +Install from PyPI:: + + pip install django-constance + +Or install the `in-development version`_ using ``pip``:: pip install -e git+git://github.com/aleszoulek/django-constance#egg=django-constance +.. _`in-development version`: https://github.com/aleszoulek/django-constance/tarball/master#egg=django-constance-dev + Configuration -============= +------------- Modify your ``settings.py``. Add ``constance`` to your ``INSTALLED_APPS``, -point ``CONSTANCE_CONNECTION`` to your Redis instance, and move each -key you want to turn dynamic into the ``CONSTANCE_CONFIG`` section, like this:: - +and move each key you want to turn dynamic into the ``CONSTANCE_CONFIG`` +section, like this:: INSTALLED_APPS = ( ... 'constance', ) - CONSTANCE_CONNECTION = { - 'host': 'localhost', - 'port': 6379, - 'db': 0, - } - - CONSTANCE_CONFIG = { 'MY_SETTINGS_KEY': (42, 'the answer to everything'), } @@ -41,10 +40,61 @@ Here, ``42`` is the default value for the key MY_SETTINGS_KEY if it is not found in Redis. The other member of the tuple is a help text the admin will show. -Usage -===== +See the `Backends`_ section how to setup the backend. -:: +Backends +~~~~~~~~ + +Constance ships with a series of backends that are used to store the +configuration values: + +* ``constance.backends.redis.RedisBackend`` (default) + + The is the default backend and has a couple of options: + + * ``CONSTANCE_REDIS_CONNECTION``: a dictionary of parameters to pass to + the to Redis client, e.g.:: + + CONSTANCE_REDIS_CONNECTION = { + 'host': 'localhost', + 'port': 6379, + 'db': 0, + } + + * ``CONSTANCE_REDIS_CONNECTION_CLASS`` (optional): an dotted import + path to a connection to use, e.g.:: + + CONSTANCE_REDIS_CONNECTION_CLASS = 'myproject.myapp.mockup.Connection' + + * ``CONSTANCE_REDIS_PREFIX`` (optional): the prefix to be used for the + key when storing in the Redis database. Defaults to ``constance:``. E.g.:: + + CONSTANCE_REDIS_PREFIX = 'constance:myproject:' + +* ``constance.backends.database.DatabaseBackend`` + + If you want to use this backend you need to add + ``'constance.backends.databse'`` to you ``INSTALLED_APPS`` setting. + + It also uses `django-picklefield`_ to store the values in the database, so + you need to install this library, too. E.g.:: + + pip install django-picklefield + + The database backend will automatically cache the config values in memory + and clear them when during saving. + +.. _django-picklefield: http://pypi.python.org/pypi/django-picklefield/ + +Usage +----- + +Constance can be used from your Python code and from your Django templates. + +* Python + + Accessing the config variables is as easy as importing the config + object and accessing the variables with attribute lookups:: from constance import config @@ -53,12 +103,46 @@ Usage if config.MY_SETTINGS_KEY == 42: answer_the_question() +* Django templates + + To access the config object from your template, you can either + pass the object to the template context:: + + from django.shortcuts import render_to_response + from constance import config + + def myview(request): + return render_to_response('my_template.html', {'config': config}) + + Or you can use the included config context processor.:: + + TEMPLATE_CONTEXT_PROCESSORS = ( + # ... + 'constance.context_processors.config', + ) + + This will add the config instance to the context of any template + rendered with a ``RequestContext``. + + Then, in you template you can refer to the config values just as + any other variable, e.g.:: + +

Welcome on {% config.SITE_NAME %}

+ {% if config.BETA_LAUNCHED %} + Woohoo! Head over here to use the beta. + {% else %} + Sadly we haven't launched yet, click here + to signup for our newletter. + {% endif %} + +Editing +~~~~~~~ Fire up your ``admin`` and you should see a new application ``Constance`` with ``MY_SETTINGS_KEY`` in the ``Config`` pseudo model. Screenshots -=========== +^^^^^^^^^^^ .. figure:: https://github.com/aleszoulek/django-constance/raw/master/docs/screenshot2.png From 90e65fe73b70851b502b2f425988fd5d992fbb9c Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 1 Dec 2010 19:29:21 +0100 Subject: [PATCH 05/14] Added myself to authors file. --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index cefd0c1..aa9b956 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,3 +3,4 @@ Vojtech Jasny Roman Krejcik Jan Vesely Ales Zoulek +Jannis Leidel From 9e70c52f8522149c1723e6e10b07af537a8a0cc6 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 1 Dec 2010 19:45:05 +0100 Subject: [PATCH 06/14] Fixed a few typos in the README. --- README.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index f8b833e..1907f0e 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Dynamic Django settings Features -------- -* Easy migrate your static settings to dynamic settings. +* Easily migrate your static settings to dynamic settings. * Admin interface to edit the dynamic settings. Installation @@ -23,7 +23,7 @@ Or install the `in-development version`_ using ``pip``:: Configuration ------------- -Modify your ``settings.py``. Add ``constance`` to your ``INSTALLED_APPS``, +Modify your ``settings.py``. Add ``'constance'`` to your ``INSTALLED_APPS``, and move each key you want to turn dynamic into the ``CONSTANCE_CONFIG`` section, like this:: @@ -36,9 +36,9 @@ section, like this:: 'MY_SETTINGS_KEY': (42, 'the answer to everything'), } -Here, ``42`` is the default value for the key MY_SETTINGS_KEY if it is not -found in Redis. The other member of the tuple is a help text the admin -will show. +Here, ``42`` is the default value for the key ``MY_SETTINGS_KEY`` if it is +not found in the backend. The other member of the tuple is a help text the +admin will show. See the `Backends`_ section how to setup the backend. @@ -67,14 +67,15 @@ configuration values: CONSTANCE_REDIS_CONNECTION_CLASS = 'myproject.myapp.mockup.Connection' * ``CONSTANCE_REDIS_PREFIX`` (optional): the prefix to be used for the - key when storing in the Redis database. Defaults to ``constance:``. E.g.:: + key when storing in the Redis database. Defaults to ``'constance:'``. + E.g.:: CONSTANCE_REDIS_PREFIX = 'constance:myproject:' * ``constance.backends.database.DatabaseBackend`` If you want to use this backend you need to add - ``'constance.backends.databse'`` to you ``INSTALLED_APPS`` setting. + ``'constance.backends.database'`` to you ``INSTALLED_APPS`` setting. It also uses `django-picklefield`_ to store the values in the database, so you need to install this library, too. E.g.:: @@ -82,7 +83,7 @@ configuration values: pip install django-picklefield The database backend will automatically cache the config values in memory - and clear them when during saving. + and clear them when when saving occurs. .. _django-picklefield: http://pypi.python.org/pypi/django-picklefield/ @@ -124,7 +125,7 @@ Constance can be used from your Python code and from your Django templates. This will add the config instance to the context of any template rendered with a ``RequestContext``. - Then, in you template you can refer to the config values just as + Then, in your template you can refer to the config values just as any other variable, e.g.::

Welcome on {% config.SITE_NAME %}

@@ -138,7 +139,7 @@ Constance can be used from your Python code and from your Django templates. Editing ~~~~~~~ -Fire up your ``admin`` and you should see a new application ``Constance`` +Fire up your ``admin`` and you should see a new app called ``Constance`` with ``MY_SETTINGS_KEY`` in the ``Config`` pseudo model. Screenshots From b1c2847997a32d6bdae42de84dd743a51cbf569e Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 3 Dec 2010 07:36:25 +0100 Subject: [PATCH 07/14] Renamed redis backend module to redisd to prevent import conflicts with redis-py. --- constance/backends/{redis.py => redisd.py} | 0 constance/settings.py | 2 +- tests/testproject/test_app/tests/test_config.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename constance/backends/{redis.py => redisd.py} (100%) diff --git a/constance/backends/redis.py b/constance/backends/redisd.py similarity index 100% rename from constance/backends/redis.py rename to constance/backends/redisd.py diff --git a/constance/settings.py b/constance/settings.py index a5948e1..d046868 100644 --- a/constance/settings.py +++ b/constance/settings.py @@ -8,7 +8,7 @@ settings = import_module_attr( PREFIX = getattr(settings, 'CONSTANCE_REDIS_PREFIX', getattr(settings, 'CONSTANCE_PREFIX', 'constance:')) -BACKEND = getattr(settings, 'CONSTANCE_BACKEND', 'constance.backends.redis.RedisBackend') +BACKEND = getattr(settings, 'CONSTANCE_BACKEND', 'constance.backends.redisd.RedisBackend') CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {}) diff --git a/tests/testproject/test_app/tests/test_config.py b/tests/testproject/test_app/tests/test_config.py index cb3675d..3b18953 100644 --- a/tests/testproject/test_app/tests/test_config.py +++ b/tests/testproject/test_app/tests/test_config.py @@ -75,7 +75,7 @@ class TestRedis(TestCase, TestStorage): def setUp(self): self.old_backend = settings.BACKEND - settings.BACKEND = 'constance.backends.redis.RedisBackend' + settings.BACKEND = 'constance.backends.redisd.RedisBackend' def tearDown(self): del sys.modules['constance'] From d6e9521ffb89840bff43825fd4ab1f50d92f4d6a Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Fri, 3 Dec 2010 20:36:56 +0100 Subject: [PATCH 08/14] Added __unicode__ to Constance model. --- constance/backends/database/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/constance/backends/database/models.py b/constance/backends/database/models.py index f5bc3c6..64819c1 100644 --- a/constance/backends/database/models.py +++ b/constance/backends/database/models.py @@ -18,3 +18,6 @@ class Constance(models.Model): verbose_name = _('constance') verbose_name_plural = _('constances') db_table = 'constance_config' + + def __unicode__(self): + return self.key From 240ef140152c9d7aec0cd0e1c624dbc5be8a20d6 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Sat, 11 Dec 2010 16:30:46 +0100 Subject: [PATCH 09/14] Refactored prefix handling in redis backend slightly. --- constance/backends/redisd.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/constance/backends/redisd.py b/constance/backends/redisd.py index ca982de..cf09b12 100644 --- a/constance/backends/redisd.py +++ b/constance/backends/redisd.py @@ -25,11 +25,14 @@ class RedisBackend(Backend): "The Redis backend requires redis-py to be installed.") self._rd = redis.Redis(**settings.REDIS_CONNECTION) + def add_prefix(self, key): + return "%s%s" % (self._prefix, key) + def get(self, key): - value = self._rd.get("%s%s" % (self._prefix, key)) + value = self._rd.get(self.add_prefix(key)) if value: return loads(value) return None def set(self, key, value): - self._rd.set("%s%s" % (self._prefix, key), dumps(value)) + self._rd.set(self.add_prefix(key), dumps(value)) From 1dd0a8b7c9ef5b1f2f5df820794003430d961b1e Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Sat, 11 Dec 2010 16:38:40 +0100 Subject: [PATCH 10/14] Added mget method to base backend to fetch all keys in one fell swoop, instead of one by one. Especially useful in the admin. --- constance/admin.py | 15 ++++++++++++--- constance/backends/__init__.py | 13 +++++++++++-- constance/backends/redisd.py | 7 +++++++ tests/testproject/test_app/redis_mockup.py | 8 +++++++- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/constance/admin.py b/constance/admin.py index b9ca7ac..dcc9b2d 100644 --- a/constance/admin.py +++ b/constance/admin.py @@ -61,8 +61,13 @@ class ConstanceAdmin(admin.ModelAdmin): @csrf_protect_m def changelist_view(self, request, extra_context=None): - form = ConstanceForm(initial=dict((name, getattr(config, name)) - for name in settings.CONFIG)) + # First load a mapping between config name and default value + default_initial = ((name, default) + for name, (default, help_text) in settings.CONFIG.iteritems()) + # Then update the mapping with actually values from the backend + initial = dict(default_initial, + **dict(config._backend.mget(settings.CONFIG.iterkeys()))) + form = ConstanceForm(initial=initial) if request.method == 'POST': form = ConstanceForm(request.POST) if form.is_valid(): @@ -79,7 +84,11 @@ class ConstanceAdmin(admin.ModelAdmin): 'media': self.media + form.media, } for name, (default, help_text) in settings.CONFIG.iteritems(): - value = getattr(config, name) + # First try to load the value from the actual backend + value = initial.get(name) + # Then if the returned value is None, get the default + if value is None: + value = getattr(config, name) context['config'].append({ 'name': name, 'default': localize(default), diff --git a/constance/backends/__init__.py b/constance/backends/__init__.py index 1eb0a02..a7f9f40 100644 --- a/constance/backends/__init__.py +++ b/constance/backends/__init__.py @@ -1,16 +1,25 @@ +""" +Defines the base constance backend +""" class Backend(object): def get(self, key): """ - Get the key from the backend store and return it. + Get the key from the backend store and return the value. Return None if not found. """ raise NotImplementedError + def mget(self, keys): + """ + Get the keys from the backend store and return a list of the values. + Return an empty list if not found. + """ + raise NotImplementedError + def set(self, key, value): """ Add the value to the backend store given the key. """ raise NotImplementedError - diff --git a/constance/backends/redisd.py b/constance/backends/redisd.py index cf09b12..d70c4a5 100644 --- a/constance/backends/redisd.py +++ b/constance/backends/redisd.py @@ -1,3 +1,5 @@ +import itertools + from django.core.exceptions import ImproperlyConfigured from constance import settings, utils @@ -34,5 +36,10 @@ class RedisBackend(Backend): return loads(value) return None + def mget(self, keys): + prefixed_keys = (self.add_prefix(key) for key in keys) + values = (loads(value) for value in self._rd.mget(prefixed_keys)) + return itertools.izip(keys, values) + def set(self, key, value): self._rd.set(self.add_prefix(key), dumps(value)) diff --git a/tests/testproject/test_app/redis_mockup.py b/tests/testproject/test_app/redis_mockup.py index 0af0017..5b0778b 100644 --- a/tests/testproject/test_app/redis_mockup.py +++ b/tests/testproject/test_app/redis_mockup.py @@ -2,4 +2,10 @@ class Connection(dict): def set(self, key, value): self[key] = value - + def mget(self, keys): + values = [] + for key in keys: + value = self.get(key, None) + if value is not None: + values.append(value) + return values From 3ef39d9185d2b4b5377180187339c0a72039e033 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Sat, 11 Dec 2010 16:43:31 +0100 Subject: [PATCH 11/14] Enabled database backend to properly require a cache backend that works in cross-process environments. This adds an optional setting CONSTANCE_DATABASE_CACHE_BACKEND. --- README.rst | 13 ++++++- constance/backends/database/__init__.py | 50 ++++++++++++++++++------- constance/settings.py | 3 ++ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 1907f0e..8e65932 100644 --- a/README.rst +++ b/README.rst @@ -82,8 +82,17 @@ configuration values: pip install django-picklefield - The database backend will automatically cache the config values in memory - and clear them when when saving occurs. + The database backend has the ability to automatically cache the config + values and clear them when saving. You need to set the following setting + to enable this feature:: + + CONSTANCE_DATABASE_CACHE_BACKEND = 'memcached://127.0.0.1:11211/' + + .. note:: + + This won't work with a cache backend that doesn't support + cross-process caching, because correct cache invalidation + can't be guaranteed. .. _django-picklefield: http://pypi.python.org/pypi/django-picklefield/ diff --git a/constance/backends/database/__init__.py b/constance/backends/database/__init__.py index 2563923..6798480 100644 --- a/constance/backends/database/__init__.py +++ b/constance/backends/database/__init__.py @@ -1,37 +1,59 @@ from django.core.exceptions import ImproperlyConfigured from django.db.models.signals import post_save from django.utils.functional import memoize +from django.core.cache import get_cache +from django.core.cache.backends.locmem import CacheClass as LocMemCacheClass from constance.backends import Backend +from constance import settings -db_cache = {} +db_cache = None +if settings.DATABASE_CACHE_BACKEND: + db_cache = get_cache(settings.DATABASE_CACHE_BACKEND) + if isinstance(db_cache, LocMemCacheClass): + raise ImproperlyConfigured( + "The CONSTANCE_DATABASE_CACHE_BACKEND setting refers to a " + "subclass of Django's local-memory backend (%r). Please set " + "it to a backend that supports cross-process caching." + % settings.DATABASE_CACHE_BACKEND) class DatabaseBackend(Backend): - def __init__(self): from constance.backends.database.models import Constance - if not Constance._meta.installed: - raise ImproperlyConfigured( - "The constance.contrib.database app isn't installed correctly. " - "Make sure it's listed in the INSTALLED_APPS setting.") self._model = Constance + if not self._model._meta.installed: + raise ImproperlyConfigured( + "The constance.backends.database app isn't installed " + "correctly. Make sure it's in your INSTALLED_APPS setting.") # Clear simple cache. post_save.connect(self.clear, sender=self._model) - def _get(self, key): - try: - value = self._model._default_manager.get(key=key).value - except self._model.DoesNotExist: - return None + def mget(self, keys): + for const in self._model._default_manager.filter(key__in=keys): + yield const.key, const.value + + def get(self, key): + value = None + if db_cache: + value = db_cache.get(key) + if value is None: + try: + value = self._model._default_manager.get(key=key).value + except self._model.DoesNotExist: + pass + else: + if db_cache: + db_cache.add(key, value) return value - get = memoize(_get, db_cache, 2) def set(self, key, value): - constance, created = self._model._default_manager.get_or_create(key=key, defaults={'value': value}) + constance, created = self._model._default_manager.get_or_create( + key=key, defaults={'value': value} + ) if not created: constance.value = value constance.save() def clear(self, sender, instance, created, **kwargs): - if not created: + if db_cache and not created: db_cache.clear() diff --git a/constance/settings.py b/constance/settings.py index d046868..83e4648 100644 --- a/constance/settings.py +++ b/constance/settings.py @@ -17,3 +17,6 @@ CONNECTION_CLASS = getattr(settings, 'CONSTANCE_REDIS_CONNECTION_CLASS', REDIS_CONNECTION = getattr(settings, 'CONSTANCE_REDIS_CONNECTION', getattr(settings, 'CONSTANCE_CONNECTION', {})) + +DATABASE_CACHE_BACKEND = getattr(settings, 'CONSTANCE_DATABASE_CACHE_BACKEND', + None) From fa83c63e83fcf509095b5f1c3d3a3a6731dfecef Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Sat, 11 Dec 2010 16:47:14 +0100 Subject: [PATCH 12/14] Removed stale file. --- constance/backends.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 constance/backends.py diff --git a/constance/backends.py b/constance/backends.py deleted file mode 100644 index e69de29..0000000 From 4e0a352b5516dcf6cd5100f2c7825220828d6c05 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Sat, 11 Dec 2010 16:49:09 +0100 Subject: [PATCH 13/14] Re-added empty models.py. --- constance/models.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 constance/models.py diff --git a/constance/models.py b/constance/models.py new file mode 100644 index 0000000..e69de29 From ada157c7df77314f0f19a39b48ad760b3f67908b Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Sat, 11 Dec 2010 22:42:41 +0100 Subject: [PATCH 14/14] Fixed a minor issues with redis-py insistence on a args as lists. --- constance/admin.py | 2 +- constance/backends/redisd.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/constance/admin.py b/constance/admin.py index dcc9b2d..68ff94c 100644 --- a/constance/admin.py +++ b/constance/admin.py @@ -66,7 +66,7 @@ class ConstanceAdmin(admin.ModelAdmin): for name, (default, help_text) in settings.CONFIG.iteritems()) # Then update the mapping with actually values from the backend initial = dict(default_initial, - **dict(config._backend.mget(settings.CONFIG.iterkeys()))) + **dict(config._backend.mget(settings.CONFIG.keys()))) form = ConstanceForm(initial=initial) if request.method == 'POST': form = ConstanceForm(request.POST) diff --git a/constance/backends/redisd.py b/constance/backends/redisd.py index d70c4a5..1c59382 100644 --- a/constance/backends/redisd.py +++ b/constance/backends/redisd.py @@ -37,7 +37,9 @@ class RedisBackend(Backend): return None def mget(self, keys): - prefixed_keys = (self.add_prefix(key) for key in keys) + if not keys: + return [] + prefixed_keys = [self.add_prefix(key) for key in keys] values = (loads(value) for value in self._rd.mget(prefixed_keys)) return itertools.izip(keys, values)