mirror of
https://github.com/jazzband/django-constance.git
synced 2026-03-16 22:40:24 +00:00
Fix constance management command without admin installed (#506)
* refactor out ConstanceForm and get_values into forms.py and utils.py respectively * fix tests and documentation * correct mock import * fix merge
This commit is contained in:
parent
92e595e68b
commit
0047a781af
9 changed files with 195 additions and 179 deletions
|
|
@ -1,187 +1,25 @@
|
|||
from collections import OrderedDict
|
||||
from datetime import datetime, date, time, timedelta
|
||||
from decimal import Decimal
|
||||
from datetime import date, datetime
|
||||
from operator import itemgetter
|
||||
import hashlib
|
||||
|
||||
from django import forms, VERSION, conf
|
||||
from django import VERSION, forms
|
||||
from django.apps import apps
|
||||
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.core.files.storage import default_storage
|
||||
from django.forms import fields
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import smart_bytes
|
||||
from django.utils.formats import localize
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.text import normalize_newlines
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.urls import path
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import LazyConfig, settings
|
||||
from .checks import get_inconsistent_fieldnames
|
||||
|
||||
from .forms import ConstanceForm
|
||||
from .utils import get_values
|
||||
|
||||
config = LazyConfig()
|
||||
|
||||
|
||||
NUMERIC_WIDGET = forms.TextInput(attrs={'size': 10})
|
||||
|
||||
INTEGER_LIKE = (fields.IntegerField, {'widget': NUMERIC_WIDGET})
|
||||
STRING_LIKE = (fields.CharField, {
|
||||
'widget': forms.Textarea(attrs={'rows': 3}),
|
||||
'required': False,
|
||||
})
|
||||
|
||||
FIELDS = {
|
||||
bool: (fields.BooleanField, {'required': False}),
|
||||
int: INTEGER_LIKE,
|
||||
Decimal: (fields.DecimalField, {'widget': NUMERIC_WIDGET}),
|
||||
str: STRING_LIKE,
|
||||
datetime: (
|
||||
fields.SplitDateTimeField, {'widget': widgets.AdminSplitDateTime}
|
||||
),
|
||||
timedelta: (
|
||||
fields.DurationField, {'widget': widgets.AdminTextInputWidget}
|
||||
),
|
||||
date: (fields.DateField, {'widget': widgets.AdminDateWidget}),
|
||||
time: (fields.TimeField, {'widget': widgets.AdminTimeWidget}),
|
||||
float: (fields.FloatField, {'widget': NUMERIC_WIDGET}),
|
||||
}
|
||||
|
||||
|
||||
def parse_additional_fields(fields):
|
||||
for key in fields:
|
||||
field = list(fields[key])
|
||||
|
||||
if len(field) == 1:
|
||||
field.append({})
|
||||
|
||||
field[0] = import_string(field[0])
|
||||
|
||||
if 'widget' in field[1]:
|
||||
klass = import_string(field[1]['widget'])
|
||||
field[1]['widget'] = klass(
|
||||
**(field[1].get('widget_kwargs', {}) or {})
|
||||
)
|
||||
|
||||
if 'widget_kwargs' in field[1]:
|
||||
del field[1]['widget_kwargs']
|
||||
|
||||
fields[key] = field
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
FIELDS.update(parse_additional_fields(settings.ADDITIONAL_FIELDS))
|
||||
|
||||
|
||||
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)))
|
||||
|
||||
return initial
|
||||
|
||||
|
||||
class ConstanceForm(forms.Form):
|
||||
version = forms.CharField(widget=forms.HiddenInput)
|
||||
|
||||
def __init__(self, initial, request=None, *args, **kwargs):
|
||||
super().__init__(*args, initial=initial, **kwargs)
|
||||
version_hash = hashlib.sha256()
|
||||
|
||||
only_view = request and not request.user.has_perm('constance.change_config')
|
||||
if only_view:
|
||||
messages.warning(
|
||||
request,
|
||||
_("You don't have permission to change these values"),
|
||||
)
|
||||
|
||||
for name, options in settings.CONFIG.items():
|
||||
default = options[0]
|
||||
if len(options) == 3:
|
||||
config_type = options[2]
|
||||
if config_type not in settings.ADDITIONAL_FIELDS and not isinstance(default, config_type):
|
||||
raise ImproperlyConfigured(_("Default value type must be "
|
||||
"equal to declared config "
|
||||
"parameter type. Please fix "
|
||||
"the default value of "
|
||||
"'%(name)s'.")
|
||||
% {'name': name})
|
||||
else:
|
||||
config_type = type(default)
|
||||
|
||||
if config_type not in FIELDS:
|
||||
raise ImproperlyConfigured(_("Constance doesn't support "
|
||||
"config values of the type "
|
||||
"%(config_type)s. Please fix "
|
||||
"the value of '%(name)s'.")
|
||||
% {'config_type': config_type,
|
||||
'name': name})
|
||||
field_class, kwargs = FIELDS[config_type]
|
||||
if only_view:
|
||||
kwargs['disabled'] = True
|
||||
self.fields[name] = field_class(label=name, **kwargs)
|
||||
|
||||
version_hash.update(smart_bytes(initial.get(name, '')))
|
||||
self.initial['version'] = version_hash.hexdigest()
|
||||
|
||||
def save(self):
|
||||
for file_field in self.files:
|
||||
file = self.cleaned_data[file_field]
|
||||
self.cleaned_data[file_field] = default_storage.save(file.name, file)
|
||||
|
||||
for name in settings.CONFIG:
|
||||
current = getattr(config, name)
|
||||
new = self.cleaned_data[name]
|
||||
|
||||
if isinstance(new, str):
|
||||
new = normalize_newlines(new)
|
||||
|
||||
if conf.settings.USE_TZ and isinstance(current, datetime) and not timezone.is_aware(current):
|
||||
current = timezone.make_aware(current)
|
||||
|
||||
if current != new:
|
||||
setattr(config, name, new)
|
||||
|
||||
def clean_version(self):
|
||||
value = self.cleaned_data['version']
|
||||
|
||||
if settings.IGNORE_ADMIN_VERSION_CHECK:
|
||||
return value
|
||||
|
||||
if value != self.initial['version']:
|
||||
raise forms.ValidationError(_('The settings have been modified '
|
||||
'by someone else. Please reload the '
|
||||
'form and resubmit your changes.'))
|
||||
return value
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if not settings.CONFIG_FIELDSETS:
|
||||
return cleaned_data
|
||||
|
||||
missing_keys, extra_keys = get_inconsistent_fieldnames()
|
||||
if missing_keys or extra_keys:
|
||||
raise forms.ValidationError(_('CONSTANCE_CONFIG_FIELDSETS is missing '
|
||||
'field(s) that exists in CONSTANCE_CONFIG.'))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class ConstanceAdmin(admin.ModelAdmin):
|
||||
change_list_template = 'admin/constance/change_list.html'
|
||||
change_list_form = ConstanceForm
|
||||
|
|
|
|||
157
constance/forms.py
Normal file
157
constance/forms.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import hashlib
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django import conf, forms
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin import widgets
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.storage import default_storage
|
||||
from django.forms import fields
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import smart_bytes
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.text import normalize_newlines
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import LazyConfig, settings
|
||||
from .checks import get_inconsistent_fieldnames
|
||||
|
||||
config = LazyConfig()
|
||||
|
||||
NUMERIC_WIDGET = forms.TextInput(attrs={'size': 10})
|
||||
|
||||
INTEGER_LIKE = (fields.IntegerField, {'widget': NUMERIC_WIDGET})
|
||||
STRING_LIKE = (fields.CharField, {
|
||||
'widget': forms.Textarea(attrs={'rows': 3}),
|
||||
'required': False,
|
||||
})
|
||||
|
||||
FIELDS = {
|
||||
bool: (fields.BooleanField, {'required': False}),
|
||||
int: INTEGER_LIKE,
|
||||
Decimal: (fields.DecimalField, {'widget': NUMERIC_WIDGET}),
|
||||
str: STRING_LIKE,
|
||||
datetime: (
|
||||
fields.SplitDateTimeField, {'widget': widgets.AdminSplitDateTime}
|
||||
),
|
||||
timedelta: (
|
||||
fields.DurationField, {'widget': widgets.AdminTextInputWidget}
|
||||
),
|
||||
date: (fields.DateField, {'widget': widgets.AdminDateWidget}),
|
||||
time: (fields.TimeField, {'widget': widgets.AdminTimeWidget}),
|
||||
float: (fields.FloatField, {'widget': NUMERIC_WIDGET}),
|
||||
}
|
||||
|
||||
def parse_additional_fields(fields):
|
||||
for key in fields:
|
||||
field = list(fields[key])
|
||||
|
||||
if len(field) == 1:
|
||||
field.append({})
|
||||
|
||||
field[0] = import_string(field[0])
|
||||
|
||||
if 'widget' in field[1]:
|
||||
klass = import_string(field[1]['widget'])
|
||||
field[1]['widget'] = klass(
|
||||
**(field[1].get('widget_kwargs', {}) or {})
|
||||
)
|
||||
|
||||
if 'widget_kwargs' in field[1]:
|
||||
del field[1]['widget_kwargs']
|
||||
|
||||
fields[key] = field
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
FIELDS.update(parse_additional_fields(settings.ADDITIONAL_FIELDS))
|
||||
|
||||
|
||||
|
||||
class ConstanceForm(forms.Form):
|
||||
version = forms.CharField(widget=forms.HiddenInput)
|
||||
|
||||
def __init__(self, initial, request=None, *args, **kwargs):
|
||||
super().__init__(*args, initial=initial, **kwargs)
|
||||
version_hash = hashlib.sha256()
|
||||
|
||||
only_view = request and not request.user.has_perm('constance.change_config')
|
||||
if only_view:
|
||||
messages.warning(
|
||||
request,
|
||||
_("You don't have permission to change these values"),
|
||||
)
|
||||
|
||||
for name, options in settings.CONFIG.items():
|
||||
default = options[0]
|
||||
if len(options) == 3:
|
||||
config_type = options[2]
|
||||
if config_type not in settings.ADDITIONAL_FIELDS and not isinstance(default, config_type):
|
||||
raise ImproperlyConfigured(_("Default value type must be "
|
||||
"equal to declared config "
|
||||
"parameter type. Please fix "
|
||||
"the default value of "
|
||||
"'%(name)s'.")
|
||||
% {'name': name})
|
||||
else:
|
||||
config_type = type(default)
|
||||
|
||||
if config_type not in FIELDS:
|
||||
raise ImproperlyConfigured(_("Constance doesn't support "
|
||||
"config values of the type "
|
||||
"%(config_type)s. Please fix "
|
||||
"the value of '%(name)s'.")
|
||||
% {'config_type': config_type,
|
||||
'name': name})
|
||||
field_class, kwargs = FIELDS[config_type]
|
||||
if only_view:
|
||||
kwargs['disabled'] = True
|
||||
self.fields[name] = field_class(label=name, **kwargs)
|
||||
|
||||
version_hash.update(smart_bytes(initial.get(name, '')))
|
||||
self.initial['version'] = version_hash.hexdigest()
|
||||
|
||||
def save(self):
|
||||
for file_field in self.files:
|
||||
file = self.cleaned_data[file_field]
|
||||
self.cleaned_data[file_field] = default_storage.save(file.name, file)
|
||||
|
||||
for name in settings.CONFIG:
|
||||
current = getattr(config, name)
|
||||
new = self.cleaned_data[name]
|
||||
|
||||
if isinstance(new, str):
|
||||
new = normalize_newlines(new)
|
||||
|
||||
if conf.settings.USE_TZ and isinstance(current, datetime) and not timezone.is_aware(current):
|
||||
current = timezone.make_aware(current)
|
||||
|
||||
if current != new:
|
||||
setattr(config, name, new)
|
||||
|
||||
def clean_version(self):
|
||||
value = self.cleaned_data['version']
|
||||
|
||||
if settings.IGNORE_ADMIN_VERSION_CHECK:
|
||||
return value
|
||||
|
||||
if value != self.initial['version']:
|
||||
raise forms.ValidationError(_('The settings have been modified '
|
||||
'by someone else. Please reload the '
|
||||
'form and resubmit your changes.'))
|
||||
return value
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if not settings.CONFIG_FIELDSETS:
|
||||
return cleaned_data
|
||||
|
||||
missing_keys, extra_keys = get_inconsistent_fieldnames()
|
||||
if missing_keys or extra_keys:
|
||||
raise forms.ValidationError(_('CONSTANCE_CONFIG_FIELDSETS is missing '
|
||||
'field(s) that exists in CONSTANCE_CONFIG.'))
|
||||
|
||||
return cleaned_data
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
from django import VERSION
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from django.utils.translation import gettext as _
|
||||
from django import VERSION
|
||||
|
||||
|
||||
from ... import config
|
||||
from ...admin import ConstanceForm, get_values
|
||||
from ...forms import ConstanceForm
|
||||
from ...utils import get_values
|
||||
from ...models import Constance
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,23 @@
|
|||
from importlib import import_module
|
||||
|
||||
from . import LazyConfig, settings
|
||||
|
||||
config = LazyConfig()
|
||||
|
||||
def import_module_attr(path):
|
||||
package, module = path.rsplit('.', 1)
|
||||
return getattr(import_module(package), module)
|
||||
|
||||
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)))
|
||||
|
||||
return initial
|
||||
|
|
@ -391,7 +391,8 @@ settings the way you like.
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from constance.admin import ConstanceAdmin, ConstanceForm, Config
|
||||
from constance.admin import ConstanceAdmin, Config
|
||||
from constance.forms import ConstanceForm
|
||||
class CustomConfigForm(ConstanceForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,12 @@ from django.test import TestCase, RequestFactory
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from constance import settings
|
||||
from constance.admin import Config, get_values, ConstanceForm
|
||||
from constance.admin import Config
|
||||
from constance.utils import get_values
|
||||
from constance.forms import ConstanceForm
|
||||
from unittest import mock
|
||||
|
||||
|
||||
class TestAdmin(TestCase):
|
||||
model = Config
|
||||
|
||||
|
|
@ -108,8 +111,8 @@ class TestAdmin(TestCase):
|
|||
'INT_VALUE': (1, 'some int'),
|
||||
})
|
||||
@mock.patch('constance.settings.IGNORE_ADMIN_VERSION_CHECK', True)
|
||||
@mock.patch("constance.admin.ConstanceForm.save", lambda _: None)
|
||||
@mock.patch("constance.admin.ConstanceForm.is_valid", lambda _: True)
|
||||
@mock.patch("constance.forms.ConstanceForm.save", lambda _: None)
|
||||
@mock.patch("constance.forms.ConstanceForm.is_valid", lambda _: True)
|
||||
def test_submit(self):
|
||||
"""
|
||||
Test that submitting the admin page results in an http redirect when
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
from constance import settings
|
||||
from unittest import mock
|
||||
|
||||
from constance.checks import check_fieldsets, get_inconsistent_fieldnames
|
||||
from django.test import TestCase
|
||||
from constance import settings
|
||||
|
||||
|
||||
class ChecksTestCase(TestCase):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from constance.admin import ConstanceForm
|
||||
from constance.forms import ConstanceForm
|
||||
from django.forms import fields
|
||||
from django.test import TestCase
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from constance.admin import get_values
|
||||
from constance.utils import get_values
|
||||
from constance.management.commands.constance import _set_constance_value
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
|
|
|||
Loading…
Reference in a new issue