Skip to content

Commit

Permalink
Fix constance management command without admin installed (#506)
Browse files Browse the repository at this point in the history
* refactor out ConstanceForm and get_values into forms.py and utils.py respectively

* fix tests and documentation

* correct mock import

* fix merge
  • Loading branch information
syre authored Apr 7, 2023
1 parent 92e595e commit 0047a78
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 178 deletions.
174 changes: 6 additions & 168 deletions constance/admin.py
Original file line number Diff line number Diff line change
@@ -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.urls import path
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 . 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
Expand Down
157 changes: 157 additions & 0 deletions constance/forms.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions constance/management/commands/constance.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down
17 changes: 17 additions & 0 deletions constance/utils.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 0047a78

Please sign in to comment.