mirror of
https://github.com/jazzband/django-constance.git
synced 2026-03-16 22:40:24 +00:00
Merge branch 'master' of https://github.com/jezdez/django-constance
This commit is contained in:
commit
d17bf804a4
13 changed files with 367 additions and 68 deletions
1
AUTHORS
1
AUTHORS
|
|
@ -3,3 +3,4 @@ Vojtech Jasny <voy@voy.cz>
|
|||
Roman Krejcik <farin@farin.cz>
|
||||
Jan Vesely <jave@janvesely.com>
|
||||
Ales Zoulek <ales.zoulek@gmail.com>
|
||||
Jannis Leidel <jannis@leidel.info>
|
||||
|
|
|
|||
142
README.rst
142
README.rst
|
|
@ -1,50 +1,110 @@
|
|||
Dynamic Django settings in Redis.
|
||||
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
|
||||
============
|
||||
------------
|
||||
|
||||
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::
|
||||
-------------
|
||||
|
||||
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::
|
||||
|
||||
INSTALLED_APPS = (
|
||||
...
|
||||
'constance',
|
||||
)
|
||||
|
||||
CONSTANCE_CONNECTION = {
|
||||
'host': 'localhost',
|
||||
'port': 6379,
|
||||
'db': 0,
|
||||
}
|
||||
|
||||
|
||||
CONSTANCE_CONFIG = {
|
||||
'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.
|
||||
|
||||
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.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.::
|
||||
|
||||
pip install django-picklefield
|
||||
|
||||
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/
|
||||
|
||||
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 +113,46 @@ Usage
|
|||
if config.MY_SETTINGS_KEY == 42:
|
||||
answer_the_question()
|
||||
|
||||
* Django templates
|
||||
|
||||
Fire up your ``admin`` and you should see a new application ``Constance``
|
||||
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 your template you can refer to the config values just as
|
||||
any other variable, e.g.::
|
||||
|
||||
<h1>Welcome on {% config.SITE_NAME %}</h1>
|
||||
{% if config.BETA_LAUNCHED %}
|
||||
Woohoo! Head over <a href="/sekrit/">here</a> to use the beta.
|
||||
{% else %}
|
||||
Sadly we haven't launched yet, click <a href="/newsletter/">here</a>
|
||||
to signup for our newletter.
|
||||
{% endif %}
|
||||
|
||||
Editing
|
||||
~~~~~~~
|
||||
|
||||
Fire up your ``admin`` and you should see a new app called ``Constance``
|
||||
with ``MY_SETTINGS_KEY`` in the ``Config`` pseudo model.
|
||||
|
||||
Screenshots
|
||||
===========
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. figure:: https://github.com/aleszoulek/django-constance/raw/master/docs/screenshot2.png
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
@ -64,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.CONSTANCE_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.keys())))
|
||||
form = ConstanceForm(initial=initial)
|
||||
if request.method == 'POST':
|
||||
form = ConstanceForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
|
@ -81,8 +83,12 @@ class ConstanceAdmin(admin.ModelAdmin):
|
|||
'form': form,
|
||||
'media': self.media + form.media,
|
||||
}
|
||||
for name, (default, help_text) in settings.CONSTANCE_CONFIG.iteritems():
|
||||
value = getattr(config, name)
|
||||
for name, (default, help_text) in settings.CONFIG.iteritems():
|
||||
# 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),
|
||||
|
|
|
|||
25
constance/backends/__init__.py
Normal file
25
constance/backends/__init__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
Defines the base constance backend
|
||||
"""
|
||||
|
||||
class Backend(object):
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
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
|
||||
59
constance/backends/database/__init__.py
Normal file
59
constance/backends/database/__init__.py
Normal file
|
|
@ -0,0 +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 = 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
|
||||
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 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
|
||||
|
||||
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 db_cache and not created:
|
||||
db_cache.clear()
|
||||
23
constance/backends/database/models.py
Normal file
23
constance/backends/database/models.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
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'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.key
|
||||
47
constance/backends/redisd.py
Normal file
47
constance/backends/redisd.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import itertools
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
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:
|
||||
try:
|
||||
import redis
|
||||
except ImportError:
|
||||
raise ImproperlyConfigured(
|
||||
"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(self.add_prefix(key))
|
||||
if value:
|
||||
return loads(value)
|
||||
return None
|
||||
|
||||
def mget(self, 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)
|
||||
|
||||
def set(self, key, value):
|
||||
self._rd.set(self.add_prefix(key), dumps(value))
|
||||
|
|
@ -1,44 +1,29 @@
|
|||
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, utils
|
||||
|
||||
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',
|
||||
utils.import_module_attr(settings.BACKEND)())
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
default, help_text = settings.CONSTANCE_CONFIG[key]
|
||||
except KeyError, e:
|
||||
default, help_text = settings.CONFIG[key]
|
||||
except KeyError:
|
||||
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()
|
||||
|
|
|
|||
22
constance/settings.py
Normal file
22
constance/settings.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
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_REDIS_PREFIX',
|
||||
getattr(settings, 'CONSTANCE_PREFIX', 'constance:'))
|
||||
|
||||
BACKEND = getattr(settings, 'CONSTANCE_BACKEND', 'constance.backends.redisd.RedisBackend')
|
||||
|
||||
CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {})
|
||||
|
||||
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', {}))
|
||||
|
||||
DATABASE_CACHE_BACKEND = getattr(settings, 'CONSTANCE_DATABASE_CACHE_BACKEND',
|
||||
None)
|
||||
5
constance/utils.py
Normal file
5
constance/utils.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -16,6 +16,7 @@ INSTALLED_APPS = (
|
|||
'django.contrib.admin',
|
||||
|
||||
'constance',
|
||||
'constance.backends.database',
|
||||
|
||||
'testproject.test_app',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,31 @@ 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.redisd.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):
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue