Improvements settings admin management

This commit is contained in:
Marc Aymerich 2015-04-26 15:52:40 +00:00
parent 759c01c64c
commit 929d9beb5c
13 changed files with 122 additions and 61 deletions

12
TODO.md
View File

@ -294,15 +294,3 @@ https://code.djangoproject.com/ticket/24576
# TODO orchestra related services code reload: celery/uwsgi reloading find aonther way without root and implement reload
# insert settings on dashboard dynamically
# rename "edit settings" -> change settings
# View settings file
contrib/orders/models.py: if type(instance) in services:
contrib/orders/models.py: if type(instance) in services:
contrib/orders/helpers.py: if type(node) in services:
contrib/bills/admin.py: return [inline for inline in inlines if type(inline) is not BillLineInline]
contrib/bills/admin.py: return [inline for inline in inlines if type(inline) is not ClosedBillLineInline]
contrib/accounts/actions.py.save: if type(service) in registered_services:
contrib/accounts/actions.py: if type(service) in registered_services:
permissions/options.py: for func in inspect.getmembers(type(self), predicate=inspect.ismethod):

View File

@ -60,7 +60,7 @@ def get_accounts():
def get_administration_items():
childrens = []
if isinstalled('orchestra.contrib.settings'):
url = reverse('admin:settings_edit_settings')
url = reverse('admin:settings_setting_change')
childrens.append(items.MenuItem(_("Settings"), url))
if isinstalled('orchestra.contrib.services'):
url = reverse('admin:services_service_changelist')

View File

@ -259,8 +259,8 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin):
def get_inline_instances(self, request, obj=None):
inlines = super(BillAdmin, self).get_inline_instances(request, obj)
if obj and not obj.is_open:
return [inline for inline in inlines if type(inline) is not BillLineInline]
return [inline for inline in inlines if type(inline) is not ClosedBillLineInline]
return [inline for inline in inlines if not isinstance(inline, BillLineInline)]
return [inline for inline in inlines if not isinstance(inline, ClosedBillLineInline)]
def formfield_for_dbfield(self, db_field, **kwargs):
""" Make value input widget bigger """

View File

@ -87,10 +87,10 @@ DOMAINS_DEFAULT_NS = Setting('DOMAINS_DEFAULT_NS', (
DOMAINS_FORBIDDEN = Setting('DOMAINS_FORBIDDEN', '',
help_text=(
"This setting prevents users from providing random domain names, i.e. google.com"
"You can generate a 5K forbidden domains list from Alexa's top 1M"
"wget http://s3.amazonaws.com/alexa-static/top-1m.csv.zip -O /tmp/top-1m.csv.zip"
"unzip -p /tmp/top-1m.csv.zip | head -n 5000 | sed 's/^.*,//' > forbidden_domains.list"
"This setting prevents users from providing random domain names, i.e. google.com<br>"
"You can generate a 5K forbidden domains list from Alexa's top 1M:<br>"
"<tt> wget http://s3.amazonaws.com/alexa-static/top-1m.csv.zip -O /tmp/top-1m.csv.zip && "
"unzip -p /tmp/top-1m.csv.zip | head -n 5000 | sed 's/^.*,//' > forbidden_domains.list</tt><br>"
"'%(site_dir)s/forbidden_domains.list')"
)
)

View File

@ -14,7 +14,7 @@ SAAS_ENABLED_SERVICES = Setting('SAAS_ENABLED_SERVICES', (
'orchestra.contrib.saas.services.seafile.SeaFileService',
),
# lazy loading
choices=lambda : ((s.get_class_path(), s.get_class_path()) for s in saas.services.SoftwareService.get_plugins()),
choices=lambda: ((s.get_class_path(), s.get_class_path()) for s in saas.services.SoftwareService.get_plugins()),
multiple=True,
)
@ -25,7 +25,7 @@ SAAS_WORDPRESS_ADMIN_PASSWORD = Setting('SAAS_WORDPRESSMU_ADMIN_PASSWORD',
SAAS_WORDPRESS_BASE_URL = Setting('SAAS_WORDPRESS_BASE_URL',
'http://blogs.{}/'.format(ORCHESTRA_BASE_DOMAIN)
'https://blogs.{}/'.format(ORCHESTRA_BASE_DOMAIN)
)

View File

@ -3,7 +3,7 @@ from functools import partial
from django.contrib import admin, messages
from django.db import models
from django.views.generic.edit import FormView
from django.views import generic
from django.utils.translation import ngettext, ugettext_lazy as _
from orchestra.settings import Setting
@ -13,7 +13,7 @@ from . import parser
from .forms import SettingFormSet
class SettingView(FormView):
class SettingView(generic.edit.FormView):
template_name = 'admin/settings/change_form.html'
form_class = SettingFormSet
success_url = '.'
@ -38,10 +38,8 @@ class SettingView(FormView):
'default': setting.default,
'type': type(setting.default),
'value': setting.value,
'choices': setting.choices,
'setting': setting,
'app': app,
'editable': setting.editable,
'multiple': setting.multiple,
}
if app == 'ORCHESTRA':
initial_data.insert(account, initial)
@ -79,11 +77,28 @@ class SettingView(FormView):
_("%s changes successfully applied, the orchestra is going to be restarted...") % n,
n)
)
# TODO find aonther way without root and implement reload
# sys.run('echo { sleep 2 && python3 %s/manage.py reload; } &' % paths.get_site_dir(), async=True)
sys.run('{ sleep 2 && touch %s/wsgi.py; } &' % paths.get_project_dir(), async=True)
else:
messages.success(self.request, _("No changes have been detected."))
return super(SettingView, self).form_valid(form)
admin.site.register_url(r'^settings/setting/$', SettingView.as_view(), 'settings_edit_settings')
class SettingFileView(generic.TemplateView):
template_name = 'admin/settings/view.html'
def get_context_data(self, **kwargs):
context = super(SettingFileView, self).get_context_data(**kwargs)
settings_file = parser.get_settings_file()
with open(settings_file, 'r') as handler:
content = handler.read()
context.update({
'title': _("Settings file content"),
'settings_file': settings_file,
'content': content,
})
return context
admin.site.register_url(r'^settings/setting/view/$', SettingFileView.as_view(), 'settings_setting_view')
admin.site.register_url(r'^settings/setting/$', SettingView.as_view(), 'settings_setting_change')

View File

@ -48,14 +48,16 @@ class SettingForm(ReadOnlyFormMixin, forms.Form):
initial = kwargs.get('initial')
if initial:
self.setting_type = initial['type']
self.setting = initial['setting']
setting = self.setting
serialized_value = parser.serialize(initial['value'])
serialized_default = parser.serialize(initial['default'])
if not initial['editable'] or isinstance(serialized_value, parser.NotSupported):
if not setting.editable or isinstance(serialized_value, parser.NotSupported):
field = self.NON_EDITABLE
else:
choices = initial.get('choices')
choices = setting.choices
field = forms.ChoiceField
multiple = initial['multiple']
multiple = setting.multiple
if multiple:
field = partial(forms.MultipleChoiceField, widget=forms.CheckboxSelectMultiple)
if choices:
@ -68,26 +70,25 @@ class SettingForm(ReadOnlyFormMixin, forms.Form):
else:
field = self.FORMFIELD_FOR_SETTING_TYPE.get(self.setting_type, self.NON_EDITABLE)
field = deepcopy(field)
value = initial['value']
default = initial['default']
real_field = field
while isinstance(real_field, partial):
real_field = real_field.func
# Do not serialize following form types
value = initial['value']
default = initial['default']
self.changed = bool(value != default)
if real_field not in (forms.MultipleChoiceField, forms.BooleanField):
value = serialized_value
if real_field is not forms.BooleanField:
default = serialized_default
initial['value'] = value
initial['default'] = default
super(SettingForm, self).__init__(*args, **kwargs)
if initial:
self.changed = bool(value != default)
self.fields['value'] = field(label=_("value"))
if isinstance(self.fields['value'].widget, forms.Textarea):
rows = math.ceil(len(value)/65)
self.fields['value'].widget.attrs['rows'] = rows
self.fields['name'].help_text = initial['help_text']
self.fields['name'].help_text = mark_safe(setting.help_text)
self.fields['name'].widget.attrs['readonly'] = True
self.app = initial['app']
@ -101,11 +102,10 @@ class SettingForm(ReadOnlyFormMixin, forms.Form):
value = eval(value, parser.get_eval_context())
except Exception as exc:
raise ValidationError(str(exc))
self.setting.validate_value(value)
if not isinstance(value, self.setting_type):
if self.setting_type in (tuple, list) and isinstance(value, (tuple, list)):
value = self.setting_type(value)
else:
raise ValidationError("Please provide a %s." % self.setting_type.__name__)
return value

View File

@ -17,8 +17,15 @@
</div>
{% endblock %}
{% block content %}
<ul class="object-tools">
{% block object-tools-items %}
<li>
<a href="./view/" class="historylink">{% trans "View file" %}</a>
</li>
{% endblock %}
</ul>
<div>
<form method="post" action="">{% csrf_token %}
{% if diff %}
@ -69,7 +76,7 @@
{% endfor %}
{% endif %}
{{ field.errors.as_ul }}
<div style="font-family:monospace">{{ field }}{% if forloop.last %}{% if form.changed %}<div style="float:right" title="Changed">&#8224;</div>{% endif %}{% endif %}</div>
<div style="font-family:monospace">{{ field }}{% if forloop.last %}{% if form.changed %}<div style="float:right" title="Changed">*</div>{% endif %}{% endif %}</div>
<p class="help" style="max-width:100px; white-space:nowrap;">{{ field.help_text }}</p>
</td>
{% endfor %}
@ -82,5 +89,3 @@
{% endif %}
</form>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n %}
{% load url from future %}
{% load admin_urls static utils %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "orchestra/css/hide-inline-id.css" %}" />
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="../">Settings</a>
&rsaquo; View file content
</div>
{% endblock %}
{% block content %}
<div>
{% blocktrans %}
<p>Current <tt>{{ settings_file }}</tt> content.</p>
{% endblocktrans %}
<PRE>{{ content }}</PRE>
</div>
{% endblock %}

View File

@ -11,14 +11,26 @@ from IPy import IP
from ..utils.python import import_class
def all_valid(kwargs):
def all_valid(*args):
""" helper function to merge multiple validators at once """
errors = {}
for field, validator in kwargs.items():
try:
validator[0](*validator[1:])
except ValidationError as error:
errors[field] = error
if len(args) == 1:
# Dict
errors = {}
kwargs = args
for field, validator in kwargs.items():
try:
validator[0](*validator[1:])
except ValidationError as error:
errors[field] = error
else:
# List
errors = []
value, validators = args
for validator in validators:
try:
validator(value)
except ValidationError as error:
errors.append(error)
if errors:
raise ValidationError(errors)

View File

@ -84,8 +84,8 @@ class ReadOnlyFormMixin(object):
field.widget = SpanWidget()
if hasattr(self, 'instance'):
# Model form
original_value = str(getattr(self.instance, name))
original_value = getattr(self.instance, name)
else:
original_value = str(self.initial.get(name))
original_value = self.initial.get(name)
field.widget.original_value = original_value

View File

@ -13,7 +13,6 @@ class SpanWidget(forms.Widget):
Renders a value wrapped in a <span> tag.
Requires use of specific form support. (see ReadonlyForm or ReadonlyModelForm)
"""
def __init__(self, *args, **kwargs):
self.tag = kwargs.pop('tag', '<span>')
super(SpanWidget, self).__init__(*args, **kwargs)
@ -22,8 +21,8 @@ class SpanWidget(forms.Widget):
final_attrs = self.build_attrs(attrs, name=name)
original_value = self.original_value
# Display icon
if original_value in ('True', 'False') or isinstance(original_value, bool):
icon = static('admin/img/icon-%s.gif' % 'yes' if original_value else 'no')
if isinstance(original_value, bool):
icon = static('admin/img/icon-%s.gif' % ('yes' if original_value else 'no',))
return mark_safe('<img src="%s" alt="%s">' % (icon, str(original_value)))
tag = self.tag[:-1]
endtag = '/'.join((self.tag[0], self.tag[1:]))

View File

@ -1,8 +1,11 @@
from collections import OrderedDict
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from .core import validators
class Setting(object):
"""
@ -20,11 +23,12 @@ class Setting(object):
value = ("'%s'" if isinstance(value, str) else '%s') % value
return '<%s: %s>' % (self.name, value)
def __new__(cls, name, default, help_text="", choices=None, editable=True, multiple=False, call_init=False):
def __new__(cls, name, default, help_text="", choices=None, editable=True, multiple=False,
validators=[], types=[], call_init=False):
if call_init:
return super(Setting, cls).__new__(cls)
cls.settings[name] = cls(name, default, help_text=help_text, choices=choices,
editable=editable, multiple=multiple, call_init=True)
editable=editable, multiple=multiple, validators=validators, types=types, call_init=True)
return cls.get_value(name, default)
def __init__(self, *args, **kwargs):
@ -32,22 +36,31 @@ class Setting(object):
for name, value in kwargs.items():
setattr(self, name, value)
self.value = self.get_value(self.name, self.default)
self.validate_value(self.value)
self.settings[name] = self
def validate_value(self, value):
validators.all_valid(value, self.validators)
valid_types = list(self.types)
if isinstance(self.default, (list, tuple)):
valid_types.extend([list, tuple])
valid_types.append(type(self.default))
if not isinstance(value, tuple(valid_types)):
raise ValidationError("%s is not a valid type (%s)." %
(type(value).__name__, ', '.join(t.__name__ for t in valid_types))
)
@classmethod
def get_value(cls, name, default):
return getattr(cls.conf_settings, name, default)
# TODO validation, defaults to same type
ORCHESTRA_BASE_DOMAIN = Setting('ORCHESTRA_BASE_DOMAIN',
'orchestra.lan'
)
ORCHESTRA_SITE_URL = Setting('ORCHESTRA_SITE_URL', 'http://orchestra.%s' % ORCHESTRA_BASE_DOMAIN,
ORCHESTRA_SITE_URL = Setting('ORCHESTRA_SITE_URL', 'https://orchestra.%s' % ORCHESTRA_BASE_DOMAIN,
help_text=_("Domain name used when it will not be possible to infere the domain from a request."
"For example in periodic tasks.")
)