Merge remote-tracking branch 'jezdez/master' into custom_fields

This commit is contained in:
Petr Dlouhý 2015-11-13 11:44:56 +01:00
commit 55ddbad52c
24 changed files with 373 additions and 116 deletions

View file

@ -1,31 +1,21 @@
sudo: false
language: python
cache:
directories:
- $HOME/.cache/pip
install:
- pip install tox
env:
- TOXENV=py26-django-14
- TOXENV=py27-django-14
- TOXENV=py26-django-15
- TOXENV=py26-django-16
- TOXENV=py27-django-15
- TOXENV=py27-django-16
- TOXENV=py32-django-15
- TOXENV=py32-django-16
- TOXENV=py33-django-15
- TOXENV=py33-django-16
- TOXENV=py34-django-15
- TOXENV=py34-django-16
- TOXENV=pypy-django-15
- TOXENV=pypy-django-16
- TOXENV=py27-django-17
- TOXENV=py27-django-master
- TOXENV=py32-django-17
- TOXENV=py32-django-master
- TOXENV=py27-django-18
- TOXENV=py33-django-17
- TOXENV=py33-django-master
- TOXENV=py33-django-18
- TOXENV=py34-django-17
- TOXENV=py34-django-master
- TOXENV=py34-django-18
- TOXENV=pypy-django-17
- TOXENV=pypy-django-18
- TOXENV=py27-django-master
- TOXENV=py34-django-master
- TOXENV=pypy-django-master
script:
- tox

42
AUTHORS
View file

@ -1,7 +1,39 @@
Jiri Barton <jbar@hosting4u.cz>
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>
Alexander frenzel <alex@relatedworks.com>
Bouke Haarsma <bouke@webatoom.nl>
Camilo Nova <camilo.nova@gmail.com>
Charlie Hornsby <charlie.hornsby@hotmail.co.uk>
Curtis Maloney <curtis@tinbrain.net>
Dan Poirier <dpoirier@caktusgroup.com>
David Burke <dmbst32@gmail.com>
Florian Apolloner <florian@apolloner.eu>
Igor Támara <igor@axiacore.com>
Jake Merdich <jmerdich@users.noreply.github.com>
Jannis Leidel <jannis@leidel.info>
Janusz Harkot <janusz.harkot@gmail.com>
Jiri Barton <jbar@hosting4u.cz>
Jonas <jvp@jonasundderwolf.de>
Kuba Zarzycki <jakubzarzycki@gmail.com>
Leandra Finger <leandra.finger@gmail.com>
Les Orchard <me@lmorchard.com>
Lin Xianyi <iynaix@gmail.com>
Marcin Baran <marcin.baran@agencjawmc.pl>
Mario Orlandi <morlandi@brainstorm.it>
Mario Rosa <mario@dwaiter.com>
Mattia Larentis <mattia@larentis.eu>
Merijn Bertels <merijn.bertels@gmail.com>
Omer Katz <omer.drow@gmail.com>
Petr Knap <dev@petrknap.cz>
Philip Neustrom <philipn@gmail.com>
Pierre-Olivier Marec <pomarec@free.fr>
Roman Krejcik <farin@farin.cz>
Silvan Spross <silvan.spross@gmail.com>
Sławek Ehlert <slafs@op.pl>
Vojtech Jasny <voy@voy.cz>
Yin Jifeng <jifeng.yin@gmail.com>
illumin-us-r3v0lution <luminaries@riseup.net>
mega <qoisone@gmail.com>
saw2th <stephen@saw2th.co.uk>
trbs <trbs@trbs.net>
vl <1844144@gmail.com>
vl <vl@u64.(none)>

View file

@ -1,4 +1,4 @@
Copyright (c) 2009-2014, Comoga and individual contributors
Copyright (c) 2009-2015, Comoga and individual contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

View file

@ -1,6 +1,6 @@
from django.utils.functional import LazyObject
__version__ = '1.0.1'
__version__ = '1.1.1'
default_app_config = 'constance.apps.ConstanceConfig'

View file

@ -3,30 +3,21 @@ from decimal import Decimal
import hashlib
from operator import itemgetter
from django import forms
from django import forms, VERSION
from django.conf.urls import url
from django.contrib import admin, messages
from django.contrib.admin import widgets
from django.contrib.admin.options import csrf_protect_m
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
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.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.translation import ugettext_lazy as _
import django
try:
from django.utils.encoding import smart_bytes
except ImportError:
from django.utils.encoding import smart_str as smart_bytes
try:
from django.conf.urls import patterns, url
except ImportError: # Django < 1.4
from django.conf.urls.defaults import patterns, url
from . import LazyConfig, settings
@ -111,17 +102,18 @@ class ConstanceForm(forms.Form):
class ConstanceAdmin(admin.ModelAdmin):
change_list_template = 'admin/constance/change_list.html'
def get_urls(self):
info = self.model._meta.app_label, self.model._meta.module_name
return patterns('',
return [
url(r'^$',
self.admin_site.admin_view(self.changelist_view),
name='%s_%s_changelist' % info),
url(r'^$',
self.admin_site.admin_view(self.changelist_view),
name='%s_%s_add' % info),
)
]
@csrf_protect_m
def changelist_view(self, request, extra_context=None):
@ -146,7 +138,7 @@ class ConstanceAdmin(admin.ModelAdmin):
)
return HttpResponseRedirect('.')
context = {
'config': [],
'config_values': [],
'title': _('Constance config'),
'app_label': 'constance',
'opts': Config._meta,
@ -160,7 +152,7 @@ class ConstanceAdmin(admin.ModelAdmin):
# Then if the returned value is None, get the default
if value is None:
value = getattr(config, name)
context['config'].append({
context['config_values'].append({
'name': name,
'default': localize(default),
'help_text': _(help_text),
@ -168,11 +160,12 @@ class ConstanceAdmin(admin.ModelAdmin):
'modified': value != default,
'form_field': form[name],
})
context['config'].sort(key=itemgetter('name'))
context_instance = RequestContext(request,
current_app=self.admin_site.name)
return render_to_response('admin/constance/change_list.html',
context, context_instance=context_instance)
context['config_values'].sort(key=itemgetter('name'))
request.current_app = self.admin_site.name
# compatibility to be removed when 1.7 is deprecated
extra = {'current_app': self.admin_site.name} if VERSION < (1, 8) else {}
return TemplateResponse(request, self.change_list_template, context,
**extra)
def has_add_permission(self, *args, **kwargs):
return False
@ -192,10 +185,12 @@ class Config(object):
object_name = 'Config'
model_name = module_name = 'config'
verbose_name_plural = _('config')
get_ordered_objects = lambda x: False
abstract = False
swapped = False
def get_ordered_objects(self):
return False
def get_change_permission(self):
return 'change_%s' % self.model_name

View file

@ -1,3 +1,5 @@
from django.db.models import signals
from django import VERSION
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
@ -5,3 +7,28 @@ from django.utils.translation import ugettext_lazy as _
class ConstanceConfig(AppConfig):
name = 'constance'
verbose_name = _('Constance')
def ready(self):
super(ConstanceConfig, self).ready()
signals.post_migrate.connect(self.create_perm,
dispatch_uid='constance.create_perm')
def create_perm(self, *args, **kwargs):
"""
Creates a fake content type and permission
to be able to check for permissions
"""
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
if ContentType._meta.installed and Permission._meta.installed:
extra = {} if VERSION >= (1, 8) else {'name': 'config'}
content_type, created = ContentType.objects.get_or_create(
app_label='constance',
model='config',
**extra)
permission, created = Permission.objects.get_or_create(
name='Can change config',
content_type=content_type,
codename='change_config')

View file

@ -1,11 +1,7 @@
from django.core.cache import caches
from django.core.cache.backends.locmem import LocMemCache
from django.core.exceptions import ImproperlyConfigured
from django.db.models.signals import post_save
from django.core.cache import get_cache
try:
from django.core.cache.backends.locmem import LocMemCache
except ImportError:
from django.core.cache.backends.locmem import CacheClass as LocMemCache
from .. import Backend
from ... import settings
@ -25,7 +21,7 @@ class DatabaseBackend(Backend):
"correctly. Make sure it's in your INSTALLED_APPS setting.")
if settings.DATABASE_CACHE_BACKEND:
self._cache = get_cache(settings.DATABASE_CACHE_BACKEND)
self._cache = caches[settings.DATABASE_CACHE_BACKEND]
if isinstance(self._cache, LocMemCache):
raise ImproperlyConfigured(
"The CONSTANCE_DATABASE_CACHE_BACKEND setting refers to a "

View file

@ -3,14 +3,15 @@
# This file is distributed under the same license as the PACKAGE package.
#
# Translators:
# Yifu Yu <root@jackyyf.com>, 2015
msgid ""
msgstr ""
"Project-Id-Version: django-constance\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-27 19:05+0100\n"
"PO-Revision-Date: 2014-11-27 18:13+0000\n"
"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/django-constance/language/zh_CN/)\n"
"PO-Revision-Date: 2015-03-15 18:40+0000\n"
"Last-Translator: Yifu Yu <root@jackyyf.com>\n"
"Language-Team: Chinese (China) (http://www.transifex.com/jezdez/django-constance/language/zh_CN/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
@ -22,13 +23,13 @@ msgstr ""
msgid ""
"Constance doesn't support config values of the type %(config_type)s. Please "
"fix the value of '%(name)s'."
msgstr ""
msgstr "Constance不支持保存类型为%(config_type)s的值请修正%(name)s的值。"
#: admin.py:91
msgid ""
"The settings have been modified by someone else. Please reload the form and "
"resubmit your changes."
msgstr ""
msgstr "设置已经被他人修改过,请刷新页面并重新提交您的更改。"
#: admin.py:129
msgid "Live settings updated successfully."
@ -36,7 +37,7 @@ msgstr "成功更新实时配置"
#: admin.py:134
msgid "Constance config"
msgstr "常量配置"
msgstr "Constance 配置页面"
#: admin.py:177
msgid "config"
@ -44,15 +45,15 @@ msgstr "配置"
#: apps.py:9
msgid "Constance"
msgstr ""
msgstr "Constance模块"
#: backends/database/models.py:19
msgid "constance"
msgstr "常量"
msgstr "Constance模块"
#: backends/database/models.py:20
msgid "constances"
msgstr "常量"
msgstr "Constance模块"
#: templates/admin/constance/change_list.html:50
msgid "Name"
@ -68,7 +69,7 @@ msgstr "值"
#: templates/admin/constance/change_list.html:53
msgid "Is modified"
msgstr "是否修改"
msgstr "是否修改"
#: templates/admin/constance/change_list.html:79
msgid "Save"

View file

@ -1,24 +0,0 @@
from django.db.models import signals
def create_perm(app, created_models, verbosity, db, **kwargs):
"""
Creates a fake content type and permission
to be able to check for permissions
"""
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
if ContentType._meta.installed and Permission._meta.installed:
content_type, created = ContentType.objects.get_or_create(
name='config',
app_label='constance',
model='config')
permission, created = Permission.objects.get_or_create(
name='Can change config',
content_type=content_type,
codename='change_config')
signals.post_syncdb.connect(create_perm, dispatch_uid="constance.create_perm")

View file

@ -1,6 +1,5 @@
{% extends "admin/base_site.html" %}
{% load admin_static admin_list i18n %}
{% load url from future %}
{% block extrastyle %}
@ -61,10 +60,10 @@
<th><div class="text">{% trans "Is modified" %}</div></th>
</tr>
</thead>
{% for item in config %}
{% for item in config_values %}
<tr class="{% cycle 'row1' 'row2' %}">
<th>{{ item.name }}
<div class="help">{{ item.help_text }}</div>
<div class="help">{{ item.help_text|linebreaksbr }}</div>
</th>
<td>
{{ item.default }}

View file

@ -0,0 +1 @@
from .utils import override_config

85
constance/test/utils.py Normal file
View file

@ -0,0 +1,85 @@
from functools import wraps
from django.test import SimpleTestCase
from django.test.utils import override_settings
from .. import config
__all__ = ('override_config',)
class override_config(override_settings):
"""
Decorator to modify constance setting for TestCase.
Based on django.test.utils.override_settings.
"""
def __init__(self, **kwargs):
super(override_config, self).__init__(**kwargs)
self.original_values = {}
def __call__(self, test_func):
"""
Modify the decorated function to override config values.
"""
if isinstance(test_func, type):
if not issubclass(test_func, SimpleTestCase):
raise Exception(
"Only subclasses of Django SimpleTestCase can be "
"decorated with override_config")
return self.modify_test_case(test_func)
else:
@wraps(test_func)
def inner(*args, **kwargs):
with self:
return test_func(*args, **kwargs)
return inner
def modify_test_case(self, test_case):
"""
Override the config by modifying TestCase methods.
This method follows the Django <= 1.6 method of overriding the
_pre_setup and _post_teardown hooks rather than modifying the TestCase
itself.
"""
original_pre_setup = test_case._pre_setup
original_post_teardown = test_case._post_teardown
def _pre_setup(inner_self):
self.enable()
original_pre_setup(inner_self)
def _post_teardown(inner_self):
original_post_teardown(inner_self)
self.disable()
test_case._pre_setup = _pre_setup
test_case._post_teardown = _post_teardown
return test_case
def enable(self):
"""
Store original config values and set overridden values.
"""
# Store the original values to an instance variable
for config_key in self.options.keys():
self.original_values[config_key] = getattr(config, config_key)
# Update config with the overriden values
self.unpack_values(self.options)
def disable(self):
"""
Set original values to the config.
"""
self.unpack_values(self.original_values)
@staticmethod
def unpack_values(options):
"""
Unpack values from the given dict to config.
"""
for name, value in options.items():
setattr(config, name, value)

View file

@ -1,4 +1,4 @@
from django.utils.importlib import import_module
from importlib import import_module
def import_module_attr(path):

View file

@ -1,6 +1,33 @@
Changelog
---------
v1.1.1 (2015/10/01)
~~~~~~~~~~~~~~~~~~~
* Fixed a regression in the 1.1 release that prevented the rendering of the
admin view with constance values when using the context processor at the
same time.
v1.1 (2015/09/24)
~~~~~~~~~~~~~~~~~
* **BACKWARD INCOMPATIBLE** Dropped support for Python 2.6
The supported versions are 2.7, 3.3 (on Django < 1.9) and 3.4.
* **BACKWARD INCOMPATIBLE** Dropped support for Django 1.4, 1.5 and 1.6
The supported versions are 1.7, 1.8 and the upcoming 1.9 release
* Added compatibility to Django 1.8 and 1.9.
* Added Spanish and Chinese (``zh_CN``) translations.
* Added :class:`override_config` decorator/context manager for easy
:doc:`testing <testing>`.
* Added the ability to use linebreaks in config value help texts.
* Various testing fixes.
v1.0.1 (2015/01/07)
~~~~~~~~~~~~~~~~~~~

View file

@ -46,7 +46,7 @@ master_doc = 'index'
# General information about the project.
project = u'django-constance'
copyright = u'2014, Comoga and individual contributors'
copyright = u'2015, Comoga and individual contributors'
# The short X.Y version.
try:
@ -101,7 +101,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the

View file

@ -160,6 +160,7 @@ More documentation
:maxdepth: 2
backends
testing
changes
Indices and tables

40
docs/testing.rst Normal file
View file

@ -0,0 +1,40 @@
Testing
=======
Testing how your app behaves with different config values is achieved with the
:class:`override_config` class. This intentionally mirrors the use of Django's
:class:`~django.test.override_setting`.
.. py:class:: override_config(**kwargs)
Replaces key-value pairs in the config.
Use as decorator or context manager.
Usage
~~~~~
It can be used as a decorator at the :class:`~django.test.TestCase` level, the
method level and also as a
`context manager <https://www.python.org/dev/peps/pep-0343/>`_.
.. code-block:: python
from constance import config
from constance.test import override_config
from django.test import TestCase
@override_config(YOUR_NAME="Arthur of Camelot")
class ExampleTestCase(TestCase):
def test_what_is_your_name(self):
self.assertEqual(config.YOUR_NAME, "Arthur of Camelot")
@override_config(YOUR_QUEST="To find the Holy Grail")
def test_what_is_your_quest(self):
self.assertEqual(config.YOUR_QUEST, "To find the Holy Grail")
def test_what_is_your_favourite_color(self):
with override_config(YOUR_FAVOURITE_COLOR="Blue?"):
self.assertEqual(config.YOUR_FAVOURITE_COLOR, "Blue?")

View file

@ -72,6 +72,22 @@ TEMPLATE_LOADERS = (
# 'django.template.loaders.eggs.Loader',
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',

View file

@ -72,6 +72,7 @@ CONSTANCE_CONFIG = {
'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'),
}
DEBUG = True
@ -79,3 +80,32 @@ DEBUG = True
STATIC_ROOT = './static/'
STATIC_URL = '/static/'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'constance.context_processors.config',
],
},
},
]
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.static',
'django.core.context_processors.tz',
'django.core.context_processors.request',
'django.contrib.messages.context_processors.messages',
'constance.context_processors.config',
)

View file

@ -1,7 +1,9 @@
from django.contrib import admin
from django.contrib.auth.models import User, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.test import TestCase, RequestFactory
from django.utils import six
from constance.admin import settings, Config
@ -44,3 +46,15 @@ class TestAdmin(TestCase):
response = self.options.changelist_view(request, {})
self.assertEqual(response.status_code, 200)
def test_str(self):
ct = ContentType.objects.get(app_label='constance', model='config')
self.assertEqual(six.text_type(ct), 'config')
def test_linebreaks(self):
self.client.login(username='admin', password='nimda')
request = self.rf.get('/admin/constance/config/')
request.user = self.superuser
response = self.options.changelist_view(request, {})
self.assertContains(response, 'LINEBREAK_VALUE')
self.assertContains(response, 'eggs<br />eggs')

34
tests/test_test_utils.py Normal file
View file

@ -0,0 +1,34 @@
from django.test import TestCase
from constance import config
from constance.test import override_config
class OverrideConfigFunctionDecoratorTestCase(TestCase):
"""Test that the override_config decorator works correctly.
Test usage of override_config on test method and as context manager.
"""
def test_default_value_is_true(self):
"""Assert that the default value of config.BOOL_VALUE is True."""
self.assertTrue(config.BOOL_VALUE)
@override_config(BOOL_VALUE=False)
def test_override_config_on_method_changes_config_value(self):
"""Assert that the method decorator changes config.BOOL_VALUE."""
self.assertFalse(config.BOOL_VALUE)
def test_override_config_as_context_manager_changes_config_value(self):
"""Assert that the context manager changes config.BOOL_VALUE."""
with override_config(BOOL_VALUE=False):
self.assertFalse(config.BOOL_VALUE)
self.assertTrue(config.BOOL_VALUE)
@override_config(BOOL_VALUE=False)
class OverrideConfigClassDecoratorTestCase(TestCase):
"""Test that the override_config decorator works on classes."""
def test_override_config_on_class_changes_config_value(self):
"""Asser that the class decorator changes config.BOOL_VALUE."""
self.assertFalse(config.BOOL_VALUE)

View file

@ -1,11 +1,8 @@
from django.contrib import admin
try:
from django.conf.urls import patterns, include
except ImportError:
from django.conf.urls.defaults import patterns, include
from django.conf.urls import url, include
urlpatterns = patterns('',
(r'^admin/', include(admin.site.urls)),
)
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
]

22
tox.ini
View file

@ -1,30 +1,26 @@
[tox]
envlist =
py26-django-14,
py27-django-14,
{py26,py27,py32,py33,py34,pypy}-django-{15,16},
{py27,py32,py33,py34,pypy}-django-{17,master}
{py27,py33,py34,pypy}-django-{17,18},
{py27,py34,pypy}-django-master
[testenv]
basepython =
py26: python2.6
py27: python2.7
py32: python3.2
py33: python3.3
py34: python3.4
py35: python3.5
pypy: pypy
deps =
redis
coverage
django-picklefield
py26: unittest2
django-{14,15}: django-discover-runner
django-14: Django>=1.4,<1.5
django-15: Django>=1.5,<1.6
django-16: Django>=1.6,<1.7
django-17: Django>=1.7,<1.8
django-master: https://github.com/django/django/archive/master.zip
django-18: Django>=1.8,<1.9
django-master: https://github.com/django/django/archive/master.tar.gz
usedevelop = true
commands =
coverage run {envbindir}/django-admin.py test --settings=tests.settings -v2
coverage run {envbindir}/django-admin.py test -v2
coverage report
setenv =
PYTHONDONTWRITEBYTECODE=1
DJANGO_SETTINGS_MODULE=tests.settings