1from collections import OrderedDict 2from datetime import datetime, date, time, timedelta 3from decimal import Decimal 4from operator import itemgetter 5import hashlib 6 7from django import forms, VERSION, conf 8from django.apps import apps 9from django.conf.urls import url 10from django.contrib import admin, messages 11from django.contrib.admin import widgets 12from django.contrib.admin.options import csrf_protect_m 13from django.core.exceptions import PermissionDenied, ImproperlyConfigured 14from django.core.files.storage import default_storage 15from django.forms import fields 16from django.http import HttpResponseRedirect 17from django.template.response import TemplateResponse 18from django.utils import timezone 19from django.utils.encoding import smart_bytes 20from django.utils.formats import localize 21from django.utils.module_loading import import_string 22from django.utils.translation import ugettext_lazy as _ 23import six 24 25from . import LazyConfig, settings 26from .checks import get_inconsistent_fieldnames 27 28 29config = LazyConfig() 30 31 32NUMERIC_WIDGET = forms.TextInput(attrs={'size': 10}) 33 34INTEGER_LIKE = (fields.IntegerField, {'widget': NUMERIC_WIDGET}) 35STRING_LIKE = (fields.CharField, { 36 'widget': forms.Textarea(attrs={'rows': 3}), 37 'required': False, 38}) 39 40FIELDS = { 41 bool: (fields.BooleanField, {'required': False}), 42 int: INTEGER_LIKE, 43 Decimal: (fields.DecimalField, {'widget': NUMERIC_WIDGET}), 44 str: STRING_LIKE, 45 datetime: ( 46 fields.SplitDateTimeField, {'widget': widgets.AdminSplitDateTime} 47 ), 48 timedelta: ( 49 fields.DurationField, {'widget': widgets.AdminTextInputWidget} 50 ), 51 date: (fields.DateField, {'widget': widgets.AdminDateWidget}), 52 time: (fields.TimeField, {'widget': widgets.AdminTimeWidget}), 53 float: (fields.FloatField, {'widget': NUMERIC_WIDGET}), 54} 55 56 57def parse_additional_fields(fields): 58 for key in fields: 59 field = list(fields[key]) 60 61 if len(field) == 1: 62 field.append({}) 63 64 field[0] = import_string(field[0]) 65 66 if 'widget' in field[1]: 67 klass = import_string(field[1]['widget']) 68 field[1]['widget'] = klass( 69 **(field[1].get('widget_kwargs', {}) or {}) 70 ) 71 72 if 'widget_kwargs' in field[1]: 73 del field[1]['widget_kwargs'] 74 75 fields[key] = field 76 77 return fields 78 79 80FIELDS.update(parse_additional_fields(settings.ADDITIONAL_FIELDS)) 81 82if not six.PY3: 83 FIELDS.update({ 84 long: INTEGER_LIKE, 85 unicode: STRING_LIKE, 86 }) 87 88 89def get_values(): 90 """ 91 Get dictionary of values from the backend 92 :return: 93 """ 94 95 # First load a mapping between config name and default value 96 default_initial = ((name, options[0]) 97 for name, options in settings.CONFIG.items()) 98 # Then update the mapping with actually values from the backend 99 initial = dict(default_initial, **dict(config._backend.mget(settings.CONFIG))) 100 101 return initial 102 103 104class ConstanceForm(forms.Form): 105 version = forms.CharField(widget=forms.HiddenInput) 106 107 def __init__(self, initial, *args, **kwargs): 108 super(ConstanceForm, self).__init__(*args, initial=initial, **kwargs) 109 version_hash = hashlib.md5() 110 111 for name, options in settings.CONFIG.items(): 112 default = options[0] 113 if len(options) == 3: 114 config_type = options[2] 115 if config_type not in settings.ADDITIONAL_FIELDS and not isinstance(default, config_type): 116 raise ImproperlyConfigured(_("Default value type must be " 117 "equal to declared config " 118 "parameter type. Please fix " 119 "the default value of " 120 "'%(name)s'.") 121 % {'name': name}) 122 else: 123 config_type = type(default) 124 125 if config_type not in FIELDS: 126 raise ImproperlyConfigured(_("Constance doesn't support " 127 "config values of the type " 128 "%(config_type)s. Please fix " 129 "the value of '%(name)s'.") 130 % {'config_type': config_type, 131 'name': name}) 132 field_class, kwargs = FIELDS[config_type] 133 self.fields[name] = field_class(label=name, **kwargs) 134 135 version_hash.update(smart_bytes(initial.get(name, ''))) 136 self.initial['version'] = version_hash.hexdigest() 137 138 def save(self): 139 for file_field in self.files: 140 file = self.cleaned_data[file_field] 141 self.cleaned_data[file_field] = default_storage.save(file.name, file) 142 143 for name in settings.CONFIG: 144 current = getattr(config, name) 145 new = self.cleaned_data[name] 146 147 if conf.settings.USE_TZ and isinstance(current, datetime) and not timezone.is_aware(current): 148 current = timezone.make_aware(current) 149 150 if current != new: 151 setattr(config, name, new) 152 153 def clean_version(self): 154 value = self.cleaned_data['version'] 155 156 if settings.IGNORE_ADMIN_VERSION_CHECK: 157 return value 158 159 if value != self.initial['version']: 160 raise forms.ValidationError(_('The settings have been modified ' 161 'by someone else. Please reload the ' 162 'form and resubmit your changes.')) 163 return value 164 165 def clean(self): 166 cleaned_data = super(ConstanceForm, self).clean() 167 168 if not settings.CONFIG_FIELDSETS: 169 return cleaned_data 170 171 if get_inconsistent_fieldnames(): 172 raise forms.ValidationError(_('CONSTANCE_CONFIG_FIELDSETS is missing ' 173 'field(s) that exists in CONSTANCE_CONFIG.')) 174 175 return cleaned_data 176 177 178class ConstanceAdmin(admin.ModelAdmin): 179 change_list_template = 'admin/constance/change_list.html' 180 change_list_form = ConstanceForm 181 182 def get_urls(self): 183 info = self.model._meta.app_label, self.model._meta.module_name 184 return [ 185 url(r'^$', 186 self.admin_site.admin_view(self.changelist_view), 187 name='%s_%s_changelist' % info), 188 url(r'^$', 189 self.admin_site.admin_view(self.changelist_view), 190 name='%s_%s_add' % info), 191 ] 192 193 def get_config_value(self, name, options, form, initial): 194 default, help_text = options[0], options[1] 195 # First try to load the value from the actual backend 196 value = initial.get(name) 197 # Then if the returned value is None, get the default 198 if value is None: 199 value = getattr(config, name) 200 config_value = { 201 'name': name, 202 'default': localize(default), 203 'raw_default': default, 204 'help_text': _(help_text), 205 'value': localize(value), 206 'modified': localize(value) != localize(default), 207 'form_field': form[name], 208 'is_date': isinstance(default, date), 209 'is_datetime': isinstance(default, datetime), 210 'is_checkbox': isinstance(form[name].field.widget, forms.CheckboxInput), 211 'is_file': isinstance(form[name].field.widget, forms.FileInput), 212 } 213 214 return config_value 215 216 def get_changelist_form(self, request): 217 """ 218 Returns a Form class for use in the changelist_view. 219 """ 220 # Defaults to self.change_list_form in order to preserve backward 221 # compatibility 222 return self.change_list_form 223 224 @csrf_protect_m 225 def changelist_view(self, request, extra_context=None): 226 if not self.has_change_permission(request, None): 227 raise PermissionDenied 228 initial = get_values() 229 form_cls = self.get_changelist_form(request) 230 form = form_cls(initial=initial) 231 if request.method == 'POST': 232 form = form_cls( 233 data=request.POST, files=request.FILES, initial=initial 234 ) 235 if form.is_valid(): 236 form.save() 237 messages.add_message( 238 request, 239 messages.SUCCESS, 240 _('Live settings updated successfully.'), 241 ) 242 return HttpResponseRedirect('.') 243 context = dict( 244 self.admin_site.each_context(request), 245 config_values=[], 246 title=self.model._meta.app_config.verbose_name, 247 app_label='constance', 248 opts=self.model._meta, 249 form=form, 250 media=self.media + form.media, 251 icon_type='gif' if VERSION < (1, 9) else 'svg', 252 ) 253 for name, options in settings.CONFIG.items(): 254 context['config_values'].append( 255 self.get_config_value(name, options, form, initial) 256 ) 257 258 if settings.CONFIG_FIELDSETS: 259 context['fieldsets'] = [] 260 for fieldset_title, fields_list in settings.CONFIG_FIELDSETS.items(): 261 absent_fields = [field for field in fields_list 262 if field not in settings.CONFIG] 263 assert not any(absent_fields), ( 264 "CONSTANCE_CONFIG_FIELDSETS contains field(s) that does " 265 "not exist: %s" % ', '.join(absent_fields)) 266 267 config_values = [] 268 269 for name in fields_list: 270 options = settings.CONFIG.get(name) 271 if options: 272 config_values.append( 273 self.get_config_value(name, options, form, initial) 274 ) 275 276 context['fieldsets'].append({ 277 'title': fieldset_title, 278 'config_values': config_values 279 }) 280 if not isinstance(settings.CONFIG_FIELDSETS, OrderedDict): 281 context['fieldsets'].sort(key=itemgetter('title')) 282 283 if not isinstance(settings.CONFIG, OrderedDict): 284 context['config_values'].sort(key=itemgetter('name')) 285 request.current_app = self.admin_site.name 286 return TemplateResponse(request, self.change_list_template, context) 287 288 def has_add_permission(self, *args, **kwargs): 289 return False 290 291 def has_delete_permission(self, *args, **kwargs): 292 return False 293 294 def has_change_permission(self, request, obj=None): 295 if settings.SUPERUSER_ONLY: 296 return request.user.is_superuser 297 return super(ConstanceAdmin, self).has_change_permission(request, obj) 298 299 300class Config(object): 301 class Meta(object): 302 app_label = 'constance' 303 object_name = 'Config' 304 model_name = module_name = 'config' 305 verbose_name_plural = _('config') 306 abstract = False 307 swapped = False 308 309 def get_ordered_objects(self): 310 return False 311 312 def get_change_permission(self): 313 return 'change_%s' % self.model_name 314 315 @property 316 def app_config(self): 317 return apps.get_app_config(self.app_label) 318 319 @property 320 def label(self): 321 return '%s.%s' % (self.app_label, self.object_name) 322 323 @property 324 def label_lower(self): 325 return '%s.%s' % (self.app_label, self.model_name) 326 327 _meta = Meta() 328 329 330admin.site.register([Config], ConstanceAdmin) 331