mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-22 16:56:57 +00:00
use customized constance (#4191)
This commit is contained in:
parent
749db0bdbf
commit
d2555ab137
@ -3,7 +3,6 @@ future
|
|||||||
captcha
|
captcha
|
||||||
django-compressor
|
django-compressor
|
||||||
django-statici18n
|
django-statici18n
|
||||||
django-constance
|
|
||||||
django-post_office
|
django-post_office
|
||||||
django-webpack_loader
|
django-webpack_loader
|
||||||
gunicorn
|
gunicorn
|
||||||
|
15
thirdpart/constance/__init__.py
Normal file
15
thirdpart/constance/__init__.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from django.utils.functional import LazyObject
|
||||||
|
from . import checks
|
||||||
|
|
||||||
|
__version__ = '2.4.0'
|
||||||
|
|
||||||
|
default_app_config = 'constance.apps.ConstanceConfig'
|
||||||
|
|
||||||
|
|
||||||
|
class LazyConfig(LazyObject):
|
||||||
|
def _setup(self):
|
||||||
|
from .base import Config
|
||||||
|
self._wrapped = Config()
|
||||||
|
|
||||||
|
|
||||||
|
config = LazyConfig()
|
330
thirdpart/constance/admin.py
Normal file
330
thirdpart/constance/admin.py
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
from datetime import datetime, date, time, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
from operator import itemgetter
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from django import forms, VERSION, conf
|
||||||
|
from django.apps import apps
|
||||||
|
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.core.files.storage import default_storage
|
||||||
|
from django.forms import fields
|
||||||
|
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.translation import ugettext_lazy as _
|
||||||
|
import six
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
if not six.PY3:
|
||||||
|
FIELDS.update({
|
||||||
|
long: INTEGER_LIKE,
|
||||||
|
unicode: STRING_LIKE,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
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, *args, **kwargs):
|
||||||
|
super(ConstanceForm, self).__init__(*args, initial=initial, **kwargs)
|
||||||
|
version_hash = hashlib.md5()
|
||||||
|
|
||||||
|
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]
|
||||||
|
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 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(ConstanceForm, self).clean()
|
||||||
|
|
||||||
|
if not settings.CONFIG_FIELDSETS:
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
if get_inconsistent_fieldnames():
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_urls(self):
|
||||||
|
info = self.model._meta.app_label, self.model._meta.module_name
|
||||||
|
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),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_config_value(self, name, options, form, initial):
|
||||||
|
default, help_text = options[0], options[1]
|
||||||
|
# First try to load the value from the actual backend
|
||||||
|
value = initial.get(name)
|
||||||
|
# Then if the returned value is None, get the default
|
||||||
|
if value is None:
|
||||||
|
value = getattr(config, name)
|
||||||
|
config_value = {
|
||||||
|
'name': name,
|
||||||
|
'default': localize(default),
|
||||||
|
'raw_default': default,
|
||||||
|
'help_text': _(help_text),
|
||||||
|
'value': localize(value),
|
||||||
|
'modified': localize(value) != localize(default),
|
||||||
|
'form_field': form[name],
|
||||||
|
'is_date': isinstance(default, date),
|
||||||
|
'is_datetime': isinstance(default, datetime),
|
||||||
|
'is_checkbox': isinstance(form[name].field.widget, forms.CheckboxInput),
|
||||||
|
'is_file': isinstance(form[name].field.widget, forms.FileInput),
|
||||||
|
}
|
||||||
|
|
||||||
|
return config_value
|
||||||
|
|
||||||
|
def get_changelist_form(self, request):
|
||||||
|
"""
|
||||||
|
Returns a Form class for use in the changelist_view.
|
||||||
|
"""
|
||||||
|
# Defaults to self.change_list_form in order to preserve backward
|
||||||
|
# compatibility
|
||||||
|
return self.change_list_form
|
||||||
|
|
||||||
|
@csrf_protect_m
|
||||||
|
def changelist_view(self, request, extra_context=None):
|
||||||
|
if not self.has_change_permission(request, None):
|
||||||
|
raise PermissionDenied
|
||||||
|
initial = get_values()
|
||||||
|
form_cls = self.get_changelist_form(request)
|
||||||
|
form = form_cls(initial=initial)
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = form_cls(
|
||||||
|
data=request.POST, files=request.FILES, initial=initial
|
||||||
|
)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.add_message(
|
||||||
|
request,
|
||||||
|
messages.SUCCESS,
|
||||||
|
_('Live settings updated successfully.'),
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect('.')
|
||||||
|
context = dict(
|
||||||
|
self.admin_site.each_context(request),
|
||||||
|
config_values=[],
|
||||||
|
title=self.model._meta.app_config.verbose_name,
|
||||||
|
app_label='constance',
|
||||||
|
opts=self.model._meta,
|
||||||
|
form=form,
|
||||||
|
media=self.media + form.media,
|
||||||
|
icon_type='gif' if VERSION < (1, 9) else 'svg',
|
||||||
|
)
|
||||||
|
for name, options in settings.CONFIG.items():
|
||||||
|
context['config_values'].append(
|
||||||
|
self.get_config_value(name, options, form, initial)
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings.CONFIG_FIELDSETS:
|
||||||
|
context['fieldsets'] = []
|
||||||
|
for fieldset_title, fields_list in settings.CONFIG_FIELDSETS.items():
|
||||||
|
absent_fields = [field for field in fields_list
|
||||||
|
if field not in settings.CONFIG]
|
||||||
|
assert not any(absent_fields), (
|
||||||
|
"CONSTANCE_CONFIG_FIELDSETS contains field(s) that does "
|
||||||
|
"not exist: %s" % ', '.join(absent_fields))
|
||||||
|
|
||||||
|
config_values = []
|
||||||
|
|
||||||
|
for name in fields_list:
|
||||||
|
options = settings.CONFIG.get(name)
|
||||||
|
if options:
|
||||||
|
config_values.append(
|
||||||
|
self.get_config_value(name, options, form, initial)
|
||||||
|
)
|
||||||
|
|
||||||
|
context['fieldsets'].append({
|
||||||
|
'title': fieldset_title,
|
||||||
|
'config_values': config_values
|
||||||
|
})
|
||||||
|
if not isinstance(settings.CONFIG_FIELDSETS, OrderedDict):
|
||||||
|
context['fieldsets'].sort(key=itemgetter('title'))
|
||||||
|
|
||||||
|
if not isinstance(settings.CONFIG, OrderedDict):
|
||||||
|
context['config_values'].sort(key=itemgetter('name'))
|
||||||
|
request.current_app = self.admin_site.name
|
||||||
|
return TemplateResponse(request, self.change_list_template, context)
|
||||||
|
|
||||||
|
def has_add_permission(self, *args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_delete_permission(self, *args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
if settings.SUPERUSER_ONLY:
|
||||||
|
return request.user.is_superuser
|
||||||
|
return super(ConstanceAdmin, self).has_change_permission(request, obj)
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
class Meta(object):
|
||||||
|
app_label = 'constance'
|
||||||
|
object_name = 'Config'
|
||||||
|
model_name = module_name = 'config'
|
||||||
|
verbose_name_plural = _('config')
|
||||||
|
abstract = False
|
||||||
|
swapped = False
|
||||||
|
|
||||||
|
def get_ordered_objects(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_change_permission(self):
|
||||||
|
return 'change_%s' % self.model_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def app_config(self):
|
||||||
|
return apps.get_app_config(self.app_label)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self):
|
||||||
|
return '%s.%s' % (self.app_label, self.object_name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label_lower(self):
|
||||||
|
return '%s.%s' % (self.app_label, self.model_name)
|
||||||
|
|
||||||
|
_meta = Meta()
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register([Config], ConstanceAdmin)
|
35
thirdpart/constance/apps.py
Normal file
35
thirdpart/constance/apps.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from django.db.models import signals
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
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, using=None, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a fake content type and permission
|
||||||
|
to be able to check for permissions
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
constance_dbs = getattr(settings, 'CONSTANCE_DBS', None)
|
||||||
|
if constance_dbs is not None and using not in constance_dbs:
|
||||||
|
return
|
||||||
|
if ContentType._meta.installed and Permission._meta.installed:
|
||||||
|
content_type, created = ContentType.objects.using(using).get_or_create(
|
||||||
|
app_label='constance',
|
||||||
|
model='config',
|
||||||
|
)
|
||||||
|
|
||||||
|
permission, created = Permission.objects.using(using).get_or_create(
|
||||||
|
content_type=content_type,
|
||||||
|
codename='change_config',
|
||||||
|
defaults={'name': 'Can change config'})
|
26
thirdpart/constance/backends/__init__.py
Normal file
26
thirdpart/constance/backends/__init__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
Defines the base constance backend
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Backend(object):
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
"""
|
||||||
|
Get the key from the backend store and return the value.
|
||||||
|
Return None if not found.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def mget(self, keys):
|
||||||
|
"""
|
||||||
|
Get the keys from the backend store and return a list of the values.
|
||||||
|
Return an empty list if not found.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
"""
|
||||||
|
Add the value to the backend store given the key.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
111
thirdpart/constance/backends/database/__init__.py
Normal file
111
thirdpart/constance/backends/database/__init__.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
from django.core.cache import caches
|
||||||
|
from django.core.cache.backends.locmem import LocMemCache
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.db import OperationalError, ProgrammingError
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
|
||||||
|
from .. import Backend
|
||||||
|
from ... import settings, signals, config
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseBackend(Backend):
|
||||||
|
def __init__(self):
|
||||||
|
from .models import Constance
|
||||||
|
self._model = Constance
|
||||||
|
self._prefix = settings.DATABASE_PREFIX
|
||||||
|
self._autofill_timeout = settings.DATABASE_CACHE_AUTOFILL_TIMEOUT
|
||||||
|
self._autofill_cachekey = 'autofilled'
|
||||||
|
|
||||||
|
if not self._model._meta.installed:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"The constance.backends.database app isn't installed "
|
||||||
|
"correctly. Make sure it's in your INSTALLED_APPS setting.")
|
||||||
|
|
||||||
|
if 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 "
|
||||||
|
"subclass of Django's local-memory backend (%r). Please "
|
||||||
|
"set it to a backend that supports cross-process caching."
|
||||||
|
% settings.DATABASE_CACHE_BACKEND)
|
||||||
|
else:
|
||||||
|
self._cache = None
|
||||||
|
self.autofill()
|
||||||
|
# Clear simple cache.
|
||||||
|
post_save.connect(self.clear, sender=self._model)
|
||||||
|
|
||||||
|
def add_prefix(self, key):
|
||||||
|
return "%s%s" % (self._prefix, key)
|
||||||
|
|
||||||
|
def autofill(self):
|
||||||
|
if not self._autofill_timeout or not self._cache:
|
||||||
|
return
|
||||||
|
full_cachekey = self.add_prefix(self._autofill_cachekey)
|
||||||
|
if self._cache.get(full_cachekey):
|
||||||
|
return
|
||||||
|
autofill_values = {}
|
||||||
|
autofill_values[full_cachekey] = 1
|
||||||
|
for key, value in self.mget(settings.CONFIG):
|
||||||
|
autofill_values[self.add_prefix(key)] = value
|
||||||
|
self._cache.set_many(autofill_values, timeout=self._autofill_timeout)
|
||||||
|
|
||||||
|
def mget(self, keys):
|
||||||
|
if not keys:
|
||||||
|
return
|
||||||
|
keys = {self.add_prefix(key): key for key in keys}
|
||||||
|
try:
|
||||||
|
stored = self._model._default_manager.filter(key__in=keys)
|
||||||
|
for const in stored:
|
||||||
|
yield keys[const.key], const.value
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
key = self.add_prefix(key)
|
||||||
|
if self._cache:
|
||||||
|
value = self._cache.get(key)
|
||||||
|
if value is None:
|
||||||
|
self.autofill()
|
||||||
|
value = self._cache.get(key)
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
if value is None:
|
||||||
|
try:
|
||||||
|
value = self._model._default_manager.get(key=key).value
|
||||||
|
except (OperationalError, ProgrammingError, self._model.DoesNotExist):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if self._cache:
|
||||||
|
self._cache.add(key, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
key = self.add_prefix(key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
constance = self._model._default_manager.get(key=key)
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
# database is not created, noop
|
||||||
|
return
|
||||||
|
except self._model.DoesNotExist:
|
||||||
|
old_value = None
|
||||||
|
constance = self._model._default_manager.create(key=key, value=value)
|
||||||
|
else:
|
||||||
|
old_value = constance.value
|
||||||
|
constance.value = value
|
||||||
|
constance.save()
|
||||||
|
|
||||||
|
if self._cache:
|
||||||
|
self._cache.set(key, value)
|
||||||
|
|
||||||
|
signals.config_updated.send(
|
||||||
|
sender=config, key=key, old_value=old_value, new_value=value
|
||||||
|
)
|
||||||
|
|
||||||
|
def clear(self, sender, instance, created, **kwargs):
|
||||||
|
if self._cache and not created:
|
||||||
|
keys = [self.add_prefix(k) for k in settings.CONFIG]
|
||||||
|
keys.append(self.add_prefix(self._autofill_cachekey))
|
||||||
|
self._cache.delete_many(keys)
|
||||||
|
self.autofill()
|
@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
import picklefield.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Constance',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', primary_key=True,
|
||||||
|
auto_created=True, serialize=False)),
|
||||||
|
('key', models.CharField(unique=True, max_length=255, db_column='constance_key')),
|
||||||
|
('value', picklefield.fields.PickledObjectField(editable=False)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'constance',
|
||||||
|
'verbose_name_plural': 'constances',
|
||||||
|
'db_table': 'constance_config',
|
||||||
|
},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.1.5 on 2019-01-30 04:04
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import picklefield.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('database', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='constance',
|
||||||
|
name='value',
|
||||||
|
field=picklefield.fields.PickledObjectField(blank=True, editable=False, null=True),
|
||||||
|
),
|
||||||
|
]
|
24
thirdpart/constance/backends/database/models.py
Normal file
24
thirdpart/constance/backends/database/models.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
try:
|
||||||
|
from picklefield import PickledObjectField
|
||||||
|
except ImportError:
|
||||||
|
raise ImproperlyConfigured("Couldn't find the the 3rd party app "
|
||||||
|
"django-picklefield which is required for "
|
||||||
|
"the constance database backend.")
|
||||||
|
|
||||||
|
|
||||||
|
class Constance(models.Model):
|
||||||
|
key = models.CharField(max_length=255, unique=True, db_column='constance_key')
|
||||||
|
value = PickledObjectField(null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('constance')
|
||||||
|
verbose_name_plural = _('constances')
|
||||||
|
db_table = 'constance_config'
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.key
|
@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from south.db import db
|
||||||
|
from south.v2 import SchemaMigration
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(SchemaMigration):
|
||||||
|
|
||||||
|
def forwards(self, orm):
|
||||||
|
# Adding model 'Constance'
|
||||||
|
db.create_table('constance_config', (
|
||||||
|
('id', self.gf('django.db.models.fields.AutoField')(
|
||||||
|
primary_key=True)),
|
||||||
|
('key', self.gf('django.db.models.fields.TextField')()),
|
||||||
|
('value', self.gf('picklefield.fields.PickledObjectField')()),
|
||||||
|
))
|
||||||
|
db.send_create_signal('database', ['Constance'])
|
||||||
|
|
||||||
|
def backwards(self, orm):
|
||||||
|
# Deleting model 'Constance'
|
||||||
|
db.delete_table('constance_config')
|
||||||
|
|
||||||
|
models = {
|
||||||
|
'database.constance': {
|
||||||
|
'Meta': {'object_name': 'Constance',
|
||||||
|
'db_table': "'constance_config'"},
|
||||||
|
'id': ('django.db.models.fields.AutoField', [],
|
||||||
|
{'primary_key': 'True'}),
|
||||||
|
'key': ('django.db.models.fields.TextField', [], {}),
|
||||||
|
'value': ('picklefield.fields.PickledObjectField', [], {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete_apps = ['database']
|
@ -0,0 +1,36 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from south.db import db
|
||||||
|
from south.v2 import SchemaMigration
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(SchemaMigration):
|
||||||
|
|
||||||
|
def forwards(self, orm):
|
||||||
|
# Changing field 'Constance.key'
|
||||||
|
db.alter_column('constance_config', 'key',
|
||||||
|
self.gf('django.db.models.fields.CharField')(
|
||||||
|
max_length=255))
|
||||||
|
# Adding unique constraint on 'Constance', fields ['key']
|
||||||
|
db.create_unique('constance_config', ['key'])
|
||||||
|
|
||||||
|
def backwards(self, orm):
|
||||||
|
# Removing unique constraint on 'Constance', fields ['key']
|
||||||
|
db.delete_unique('constance_config', ['key'])
|
||||||
|
|
||||||
|
# Changing field 'Constance.key'
|
||||||
|
db.alter_column('constance_config', 'key',
|
||||||
|
self.gf('django.db.models.fields.TextField')())
|
||||||
|
|
||||||
|
models = {
|
||||||
|
'database.constance': {
|
||||||
|
'Meta': {'object_name': 'Constance',
|
||||||
|
'db_table': "'constance_config'"},
|
||||||
|
'id': ('django.db.models.fields.AutoField', [],
|
||||||
|
{'primary_key': 'True'}),
|
||||||
|
'key': ('django.db.models.fields.CharField', [],
|
||||||
|
{'unique': 'True', 'max_length': '255'}),
|
||||||
|
'value': ('picklefield.fields.PickledObjectField', [], {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
complete_apps = ['database']
|
54
thirdpart/constance/backends/redisd.py
Normal file
54
thirdpart/constance/backends/redisd.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
import six
|
||||||
|
|
||||||
|
from . import Backend
|
||||||
|
from .. import settings, utils, signals, config
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cPickle import loads, dumps
|
||||||
|
except ImportError:
|
||||||
|
from pickle import loads, dumps
|
||||||
|
|
||||||
|
|
||||||
|
class RedisBackend(Backend):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(RedisBackend, self).__init__()
|
||||||
|
self._prefix = settings.REDIS_PREFIX
|
||||||
|
connection_cls = settings.REDIS_CONNECTION_CLASS
|
||||||
|
if connection_cls is not None:
|
||||||
|
self._rd = utils.import_module_attr(connection_cls)()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
import redis
|
||||||
|
except ImportError:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"The Redis backend requires redis-py to be installed.")
|
||||||
|
if isinstance(settings.REDIS_CONNECTION, six.string_types):
|
||||||
|
self._rd = redis.from_url(settings.REDIS_CONNECTION)
|
||||||
|
else:
|
||||||
|
self._rd = redis.Redis(**settings.REDIS_CONNECTION)
|
||||||
|
|
||||||
|
def add_prefix(self, key):
|
||||||
|
return "%s%s" % (self._prefix, key)
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
value = self._rd.get(self.add_prefix(key))
|
||||||
|
if value:
|
||||||
|
return loads(value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def mget(self, keys):
|
||||||
|
if not keys:
|
||||||
|
return
|
||||||
|
prefixed_keys = [self.add_prefix(key) for key in keys]
|
||||||
|
for key, value in six.moves.zip(keys, self._rd.mget(prefixed_keys)):
|
||||||
|
if value:
|
||||||
|
yield key, loads(value)
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
old_value = self.get(key)
|
||||||
|
self._rd.set(self.add_prefix(key), dumps(value))
|
||||||
|
signals.config_updated.send(
|
||||||
|
sender=config, key=key, old_value=old_value, new_value=value
|
||||||
|
)
|
37
thirdpart/constance/base.py
Normal file
37
thirdpart/constance/base.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from . import settings, utils
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
"""
|
||||||
|
The global config wrapper that handles the backend.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super(Config, self).__setattr__('_backend',
|
||||||
|
utils.import_module_attr(settings.BACKEND)())
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
try:
|
||||||
|
if not len(settings.CONFIG[key]) in (2, 3):
|
||||||
|
raise AttributeError(key)
|
||||||
|
default = settings.CONFIG[key][0]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(key)
|
||||||
|
|
||||||
|
if not settings.ENABLED:
|
||||||
|
return default
|
||||||
|
|
||||||
|
result = self._backend.get(key)
|
||||||
|
if result is None:
|
||||||
|
result = default
|
||||||
|
# Do not set default value to db/redis when there is no result.
|
||||||
|
# setattr(self, key, default)
|
||||||
|
return result
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
if key not in settings.CONFIG:
|
||||||
|
raise AttributeError(key)
|
||||||
|
self._backend.set(key, value)
|
||||||
|
|
||||||
|
def __dir__(self):
|
||||||
|
return settings.CONFIG.keys()
|
42
thirdpart/constance/checks.py
Normal file
42
thirdpart/constance/checks.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from django.core import checks
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from . import settings
|
||||||
|
|
||||||
|
|
||||||
|
@checks.register("constance")
|
||||||
|
def check_fieldsets(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
A Django system check to make sure that, if defined, CONFIG_FIELDSETS accounts for
|
||||||
|
every entry in settings.CONFIG.
|
||||||
|
"""
|
||||||
|
if hasattr(settings, "CONFIG_FIELDSETS") and settings.CONFIG_FIELDSETS:
|
||||||
|
inconsistent_fieldnames = get_inconsistent_fieldnames()
|
||||||
|
if inconsistent_fieldnames:
|
||||||
|
return [
|
||||||
|
checks.Warning(
|
||||||
|
_(
|
||||||
|
"CONSTANCE_CONFIG_FIELDSETS is missing "
|
||||||
|
"field(s) that exists in CONSTANCE_CONFIG."
|
||||||
|
),
|
||||||
|
hint=", ".join(sorted(inconsistent_fieldnames)),
|
||||||
|
obj="settings.CONSTANCE_CONFIG",
|
||||||
|
id="constance.E001",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_inconsistent_fieldnames():
|
||||||
|
"""
|
||||||
|
Returns a set of keys from settings.CONFIG that are not accounted for in
|
||||||
|
settings.CONFIG_FIELDSETS.
|
||||||
|
If there are no fieldnames in settings.CONFIG_FIELDSETS, returns an empty set.
|
||||||
|
"""
|
||||||
|
field_name_list = []
|
||||||
|
for fieldset_title, fields_list in settings.CONFIG_FIELDSETS.items():
|
||||||
|
for field_name in fields_list:
|
||||||
|
field_name_list.append(field_name)
|
||||||
|
if not field_name_list:
|
||||||
|
return {}
|
||||||
|
return set(set(settings.CONFIG.keys()) - set(field_name_list))
|
15
thirdpart/constance/context_processors.py
Normal file
15
thirdpart/constance/context_processors.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import constance
|
||||||
|
|
||||||
|
|
||||||
|
def config(request):
|
||||||
|
"""
|
||||||
|
Simple context processor that puts the config into every
|
||||||
|
RequestContext. Just make sure you have a setting like this:
|
||||||
|
|
||||||
|
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||||
|
# ...
|
||||||
|
'constance.context_processors.config',
|
||||||
|
)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {"config": constance.config}
|
0
thirdpart/constance/management/__init__.py
Normal file
0
thirdpart/constance/management/__init__.py
Normal file
0
thirdpart/constance/management/commands/__init__.py
Normal file
0
thirdpart/constance/management/commands/__init__.py
Normal file
77
thirdpart/constance/management/commands/constance.py
Normal file
77
thirdpart/constance/management/commands/constance.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# -*- 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 django import VERSION
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
# API changed in Django>=2.1. cmd argument was removed.
|
||||||
|
parser_list = self._subparsers_add_parser(subparsers, 'list', cmd=self, help='list all Constance keys and their values')
|
||||||
|
|
||||||
|
parser_get = self._subparsers_add_parser(subparsers, '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 = self._subparsers_add_parser(subparsers, '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')
|
||||||
|
# use nargs='+' so that we pass a list to MultiValueField (eg SplitDateTimeField)
|
||||||
|
parser_set.add_argument('value', help='value to set', metavar='VALUE', nargs='+')
|
||||||
|
|
||||||
|
|
||||||
|
def _subparsers_add_parser(self, subparsers, name, **kwargs):
|
||||||
|
# API in Django >= 2.1 changed and removed cmd parameter from add_parser
|
||||||
|
if VERSION >= (2, 1) and 'cmd' in kwargs:
|
||||||
|
kwargs.pop('cmd')
|
||||||
|
return subparsers.add_parser(name, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def handle(self, command, key=None, value=None, *args, **options):
|
||||||
|
|
||||||
|
if command == 'get':
|
||||||
|
try:
|
||||||
|
self.stdout.write("{}".format(getattr(config, key)), ending="\n")
|
||||||
|
except AttributeError as e:
|
||||||
|
raise CommandError(key + " is not defined in settings.CONSTANCE_CONFIG")
|
||||||
|
|
||||||
|
elif command == 'set':
|
||||||
|
try:
|
||||||
|
if len(value) == 1:
|
||||||
|
# assume that if a single argument was passed, the field doesn't expect a list
|
||||||
|
value = value[0]
|
||||||
|
|
||||||
|
_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), ending="\n")
|
47
thirdpart/constance/settings.py
Normal file
47
thirdpart/constance/settings.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
BACKEND = getattr(
|
||||||
|
settings,
|
||||||
|
'CONSTANCE_BACKEND',
|
||||||
|
'constance.backends.redisd.RedisBackend'
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG = getattr(settings, 'CONSTANCE_CONFIG', {})
|
||||||
|
|
||||||
|
CONFIG_FIELDSETS = getattr(settings, 'CONSTANCE_CONFIG_FIELDSETS', {})
|
||||||
|
|
||||||
|
ADDITIONAL_FIELDS = getattr(settings, 'CONSTANCE_ADDITIONAL_FIELDS', {})
|
||||||
|
|
||||||
|
DATABASE_CACHE_BACKEND = getattr(
|
||||||
|
settings,
|
||||||
|
'CONSTANCE_DATABASE_CACHE_BACKEND',
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
DATABASE_CACHE_AUTOFILL_TIMEOUT = getattr(
|
||||||
|
settings,
|
||||||
|
'CONSTANCE_DATABASE_CACHE_AUTOFILL_TIMEOUT',
|
||||||
|
60 * 60 * 24
|
||||||
|
)
|
||||||
|
|
||||||
|
DATABASE_PREFIX = getattr(settings, 'CONSTANCE_DATABASE_PREFIX', '')
|
||||||
|
|
||||||
|
REDIS_PREFIX = getattr(settings, 'CONSTANCE_REDIS_PREFIX', 'constance:')
|
||||||
|
|
||||||
|
REDIS_CONNECTION_CLASS = getattr(
|
||||||
|
settings,
|
||||||
|
'CONSTANCE_REDIS_CONNECTION_CLASS',
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
REDIS_CONNECTION = getattr(settings, 'CONSTANCE_REDIS_CONNECTION', {})
|
||||||
|
|
||||||
|
SUPERUSER_ONLY = getattr(settings, 'CONSTANCE_SUPERUSER_ONLY', True)
|
||||||
|
|
||||||
|
IGNORE_ADMIN_VERSION_CHECK = getattr(
|
||||||
|
settings,
|
||||||
|
'CONSTANCE_IGNORE_ADMIN_VERSION_CHECK',
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
ENABLED = getattr(settings, 'CONSTANCE_ENABLED', True)
|
5
thirdpart/constance/signals.py
Normal file
5
thirdpart/constance/signals.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import django.dispatch
|
||||||
|
|
||||||
|
config_updated = django.dispatch.Signal(
|
||||||
|
providing_args=['key', 'old_value', 'new_value']
|
||||||
|
)
|
1
thirdpart/constance/test/__init__.py
Normal file
1
thirdpart/constance/test/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .utils import override_config
|
85
thirdpart/constance/test/utils.py
Normal file
85
thirdpart/constance/test/utils.py
Normal 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:
|
||||||
|
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)
|
6
thirdpart/constance/utils.py
Normal file
6
thirdpart/constance/utils.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
|
||||||
|
def import_module_attr(path):
|
||||||
|
package, module = path.rsplit('.', 1)
|
||||||
|
return getattr(import_module(package), module)
|
Loading…
Reference in New Issue
Block a user