diff --git a/constance/admin.py b/constance/admin.py index e36ef8b..bcbaba5 100644 --- a/constance/admin.py +++ b/constance/admin.py @@ -80,6 +80,22 @@ if not six.PY3: }) +def get_values(): + """ + Get dictionary of values from the backend + :return: + """ + + # First load a mapping between config name and default value + 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()))) + + return initial + + class ConstanceForm(forms.Form): version = forms.CharField(widget=forms.HiddenInput) @@ -167,17 +183,9 @@ class ConstanceAdmin(admin.ModelAdmin): @csrf_protect_m def changelist_view(self, request, extra_context=None): - # First load a mapping between config name and default value if not self.has_change_permission(request, None): raise PermissionDenied - 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())) - ) + initial = get_values() form = self.change_list_form(initial=initial) if request.method == 'POST': form = self.change_list_form(data=request.POST, initial=initial) diff --git a/constance/management/__init__.py b/constance/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/constance/management/commands/__init__.py b/constance/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/constance/management/commands/constance.py b/constance/management/commands/constance.py new file mode 100644 index 0000000..6f57d90 --- /dev/null +++ b/constance/management/commands/constance.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.core.exceptions import ValidationError +from django.core.management import BaseCommand, CommandError +from django.utils.translation import ugettext as _ + +from ... import config +from ...admin import ConstanceForm, get_values + + +def _set_constance_value(key, value): + """ + Parses and sets a Constance value from a string + :param key: + :param value: + :return: + """ + + form = ConstanceForm(initial=get_values()) + + field = form.fields[key] + + clean_value = field.clean(field.to_python(value)) + setattr(config, key, clean_value) + + +class Command(BaseCommand): + help = _('Get/Set In-database config settings handled by Constance') + + def add_arguments(self, parser): + subparsers = parser.add_subparsers(dest='command') + + parser_list = subparsers.add_parser('list', cmd=self, help='list all Constance keys and their values') + + parser_get = subparsers.add_parser('get', cmd=self, help='get the value of a Constance key') + parser_get.add_argument('key', help='name of the key to get', metavar='KEY') + + parser_set = subparsers.add_parser('set', cmd=self, help='set the value of a Constance key') + parser_set.add_argument('key', help='name of the key to get', metavar='KEY') + parser_set.add_argument('value', help='value to set', metavar='VALUE') + + def handle(self, command, key=None, value=None, *args, **options): + + if command == 'get': + try: + self.stdout.write("{}".format(getattr(config, key)).encode('utf-8'), ending=b"\n") + except AttributeError as e: + raise CommandError(key + " is not defined in settings.CONSTANCE_CONFIG") + + elif command == 'set': + try: + _set_constance_value(key, value) + except KeyError as e: + raise CommandError(key + " is not defined in settings.CONSTANCE_CONFIG") + except ValidationError as e: + raise CommandError(", ".join(e)) + + elif command == 'list': + for k, v in get_values().items(): + self.stdout.write("{}\t{}".format(k, v).encode('utf-8'), ending=b"\n") diff --git a/docs/index.rst b/docs/index.rst index 048cba6..9cb4120 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -143,7 +143,7 @@ Note: Use later evaluated strings instead of direct classes for the field and wi } Ordered Fields in Django Admin ------------------------ +------------------------------ In order to Order the fields , you can use OrderedDict collection. Here is an example: @@ -234,6 +234,45 @@ any other variable, e.g.: to signup for our newletter. {% endif %} +Command Line +^^^^^^^^^^^^ + +Constance settings can be get/set on the command line with the manage command `constance` + +Available options are: + +list - output all values in a tab-separated format:: + + $ ./manage.py constance list + THE_ANSWER 42 + SITE_NAME My Title + +get KEY - output a single values:: + + $ ./manage.py constance get THE_ANSWER + 42 + +set KEY VALUE - set a single value:: + + $ ./manage.py constance set SITE_NAME "Another Title" + +If the value contains spaces it should be wrapped in quotes. + +.. note:: Set values are validated as per in admin, an error will be raised if validation fails: + +Eg, given this config as per the example app:: + + CONSTANCE_CONFIG = { + ... + 'DATE_ESTABLISHED': (date(1972, 11, 30), "the shop's first opening"), + } + +Then setting an invalid date will fail as follow:: + + $ ./manage.py constance set DATE_ESTABLISHED '1999-12-00' + CommandError: Enter a valid date. + + Editing ------- diff --git a/tests/settings.py b/tests/settings.py index fb65aab..857517c 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -64,7 +64,7 @@ CONSTANCE_CONFIG = { 'LONG_VALUE': (long_value, 'some looong int'), 'BOOL_VALUE': (True, 'true or false'), 'STRING_VALUE': ('Hello world', 'greetings'), - 'UNICODE_VALUE': (six.u('Rivière-Bonjour'), 'greetings'), + 'UNICODE_VALUE': (u'Rivière-Bonjour', 'greetings'), 'DECIMAL_VALUE': (Decimal('0.1'), 'the first release version'), 'DATETIME_VALUE': (datetime(2010, 8, 23, 11, 29, 24), 'time of the first commit'), diff --git a/tests/storage.py b/tests/storage.py index fecb782..3946b3e 100644 --- a/tests/storage.py +++ b/tests/storage.py @@ -22,7 +22,7 @@ class StorageTestsMixin(object): self.assertEqual(self.config.LONG_VALUE, long(123456)) self.assertEqual(self.config.BOOL_VALUE, True) self.assertEqual(self.config.STRING_VALUE, 'Hello world') - self.assertEqual(self.config.UNICODE_VALUE, six.u('Rivière-Bonjour')) + self.assertEqual(self.config.UNICODE_VALUE, u'Rivière-Bonjour') self.assertEqual(self.config.DECIMAL_VALUE, Decimal('0.1')) self.assertEqual(self.config.DATETIME_VALUE, datetime(2010, 8, 23, 11, 29, 24)) self.assertEqual(self.config.FLOAT_VALUE, 3.1415926536) @@ -36,7 +36,7 @@ class StorageTestsMixin(object): self.config.LONG_VALUE = long(654321) self.config.BOOL_VALUE = False self.config.STRING_VALUE = 'Beware the weeping angel' - self.config.UNICODE_VALUE = six.u('Québec') + self.config.UNICODE_VALUE = u'Québec' self.config.DECIMAL_VALUE = Decimal('1.2') self.config.DATETIME_VALUE = datetime(1977, 10, 2) self.config.FLOAT_VALUE = 2.718281845905 @@ -50,7 +50,7 @@ class StorageTestsMixin(object): self.assertEqual(self.config.LONG_VALUE, long(654321)) self.assertEqual(self.config.BOOL_VALUE, False) self.assertEqual(self.config.STRING_VALUE, 'Beware the weeping angel') - self.assertEqual(self.config.UNICODE_VALUE, six.u('Québec')) + self.assertEqual(self.config.UNICODE_VALUE, u'Québec') self.assertEqual(self.config.DECIMAL_VALUE, Decimal('1.2')) self.assertEqual(self.config.DATETIME_VALUE, datetime(1977, 10, 2)) self.assertEqual(self.config.FLOAT_VALUE, 2.718281845905) @@ -74,7 +74,7 @@ class StorageTestsMixin(object): # set some values and leave out others self.config.LONG_VALUE = long(654321) self.config.BOOL_VALUE = False - self.config.UNICODE_VALUE = six.u('Québec') + self.config.UNICODE_VALUE = u'Québec' self.config.DECIMAL_VALUE = Decimal('1.2') self.config.DATETIME_VALUE = datetime(1977, 10, 2) self.config.DATE_VALUE = date(2001, 12, 20) @@ -84,7 +84,7 @@ class StorageTestsMixin(object): self.assertEqual(self.config.LONG_VALUE, long(654321)) self.assertEqual(self.config.BOOL_VALUE, False) self.assertEqual(self.config.STRING_VALUE, 'Hello world') # this should be the default value - self.assertEqual(self.config.UNICODE_VALUE, six.u('Québec')) + self.assertEqual(self.config.UNICODE_VALUE, u'Québec') self.assertEqual(self.config.DECIMAL_VALUE, Decimal('1.2')) self.assertEqual(self.config.DATETIME_VALUE, datetime(1977, 10, 2)) self.assertEqual(self.config.FLOAT_VALUE, 3.1415926536) # this should be the default value diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..1d2dfdd --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +from textwrap import dedent + +from django.core.management import call_command, CommandError +from django.test import TransactionTestCase +from django.utils.encoding import smart_str +from django.utils.six import StringIO + +from constance import config + + +class CliTestCase(TransactionTestCase): + + def setUp(self): + self.out = StringIO() + + def test_help(self): + try: + call_command('constance', '--help') + except SystemExit: + pass + + def test_list(self): + call_command('constance', 'list', stdout=self.out) + + self.assertEqual(set(self.out.getvalue().splitlines()), set(dedent(smart_str( +u""" BOOL_VALUE True + EMAIL_VALUE test@example.com + INT_VALUE 1 + LINEBREAK_VALUE Spam spam + DATE_VALUE 2010-12-24 + TIME_VALUE 23:59:59 + LONG_VALUE 123456 + STRING_VALUE Hello world + UNICODE_VALUE Rivière-Bonjour + CHOICE_VALUE yes + DECIMAL_VALUE 0.1 + DATETIME_VALUE 2010-08-23 11:29:24 + FLOAT_VALUE 3.1415926536 +""")).splitlines())) + + def test_get(self): + call_command('constance', *('get EMAIL_VALUE'.split()), stdout=self.out) + + self.assertEqual(self.out.getvalue().strip(), "test@example.com") + + def test_set(self): + call_command('constance', *('set EMAIL_VALUE blah@example.com'.split()), stdout=self.out) + + self.assertEqual(config.EMAIL_VALUE, "blah@example.com") + + def test_get_invalid_name(self): + self.assertRaisesMessage(CommandError, "NOT_A_REAL_CONFIG is not defined in settings.CONSTANCE_CONFIG", + call_command, 'constance', 'get', 'NOT_A_REAL_CONFIG') + + def test_set_invalid_name(self): + self.assertRaisesMessage(CommandError, "NOT_A_REAL_CONFIG is not defined in settings.CONSTANCE_CONFIG", + call_command, 'constance', 'set', 'NOT_A_REAL_CONFIG', 'foo') + + def test_set_invalid_value(self): + self.assertRaisesMessage(CommandError, "Enter a valid email address.", + call_command, 'constance', 'set', 'EMAIL_VALUE', 'not a valid email') diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..fd9adc4 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +import datetime +from decimal import Decimal + +from constance.admin import get_values +from constance.management.commands.constance import _set_constance_value +from django.core.exceptions import ValidationError +from django.test import TestCase + + +class UtilsTestCase(TestCase): + + def test_set_value_validation(self): + self.assertRaisesMessage(ValidationError, 'Enter a whole number.', _set_constance_value, 'INT_VALUE', 'foo') + self.assertRaisesMessage(ValidationError, 'Enter a valid email address.', _set_constance_value, 'EMAIL_VALUE', 'not a valid email') + + def test_get_values(self): + + self.assertEqual(get_values(), { + 'FLOAT_VALUE': 3.1415926536, + 'BOOL_VALUE': True, + 'EMAIL_VALUE': 'test@example.com', + 'INT_VALUE': 1, + 'CHOICE_VALUE': 'yes', + 'TIME_VALUE': datetime.time(23, 59, 59), + 'DATE_VALUE': datetime.date(2010, 12, 24), + 'LINEBREAK_VALUE': 'Spam spam', + 'DECIMAL_VALUE': Decimal('0.1'), + 'STRING_VALUE': 'Hello world', + 'UNICODE_VALUE': u'Rivière-Bonjour', + 'DATETIME_VALUE': datetime.datetime(2010, 8, 23, 11, 29, 24), + 'LONG_VALUE': 123456 + })