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 # TODO orchestra related services code reload: celery/uwsgi reloading find aonther way without root and implement reload
# insert settings on dashboard dynamically # 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(): def get_administration_items():
childrens = [] childrens = []
if isinstalled('orchestra.contrib.settings'): if isinstalled('orchestra.contrib.settings'):
url = reverse('admin:settings_edit_settings') url = reverse('admin:settings_setting_change')
childrens.append(items.MenuItem(_("Settings"), url)) childrens.append(items.MenuItem(_("Settings"), url))
if isinstalled('orchestra.contrib.services'): if isinstalled('orchestra.contrib.services'):
url = reverse('admin:services_service_changelist') url = reverse('admin:services_service_changelist')

View File

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

View File

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

View File

@ -25,7 +25,7 @@ SAAS_WORDPRESS_ADMIN_PASSWORD = Setting('SAAS_WORDPRESSMU_ADMIN_PASSWORD',
SAAS_WORDPRESS_BASE_URL = Setting('SAAS_WORDPRESS_BASE_URL', 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.contrib import admin, messages
from django.db import models 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 django.utils.translation import ngettext, ugettext_lazy as _
from orchestra.settings import Setting from orchestra.settings import Setting
@ -13,7 +13,7 @@ from . import parser
from .forms import SettingFormSet from .forms import SettingFormSet
class SettingView(FormView): class SettingView(generic.edit.FormView):
template_name = 'admin/settings/change_form.html' template_name = 'admin/settings/change_form.html'
form_class = SettingFormSet form_class = SettingFormSet
success_url = '.' success_url = '.'
@ -38,10 +38,8 @@ class SettingView(FormView):
'default': setting.default, 'default': setting.default,
'type': type(setting.default), 'type': type(setting.default),
'value': setting.value, 'value': setting.value,
'choices': setting.choices, 'setting': setting,
'app': app, 'app': app,
'editable': setting.editable,
'multiple': setting.multiple,
} }
if app == 'ORCHESTRA': if app == 'ORCHESTRA':
initial_data.insert(account, initial) initial_data.insert(account, initial)
@ -79,11 +77,28 @@ class SettingView(FormView):
_("%s changes successfully applied, the orchestra is going to be restarted...") % n, _("%s changes successfully applied, the orchestra is going to be restarted...") % n,
n) n)
) )
# TODO find aonther way without root and implement reload sys.run('{ sleep 2 && touch %s/wsgi.py; } &' % paths.get_project_dir(), async=True)
# sys.run('echo { sleep 2 && python3 %s/manage.py reload; } &' % paths.get_site_dir(), async=True)
else: else:
messages.success(self.request, _("No changes have been detected.")) messages.success(self.request, _("No changes have been detected."))
return super(SettingView, self).form_valid(form) 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') initial = kwargs.get('initial')
if initial: if initial:
self.setting_type = initial['type'] self.setting_type = initial['type']
self.setting = initial['setting']
setting = self.setting
serialized_value = parser.serialize(initial['value']) serialized_value = parser.serialize(initial['value'])
serialized_default = parser.serialize(initial['default']) 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 field = self.NON_EDITABLE
else: else:
choices = initial.get('choices') choices = setting.choices
field = forms.ChoiceField field = forms.ChoiceField
multiple = initial['multiple'] multiple = setting.multiple
if multiple: if multiple:
field = partial(forms.MultipleChoiceField, widget=forms.CheckboxSelectMultiple) field = partial(forms.MultipleChoiceField, widget=forms.CheckboxSelectMultiple)
if choices: if choices:
@ -68,26 +70,25 @@ class SettingForm(ReadOnlyFormMixin, forms.Form):
else: else:
field = self.FORMFIELD_FOR_SETTING_TYPE.get(self.setting_type, self.NON_EDITABLE) field = self.FORMFIELD_FOR_SETTING_TYPE.get(self.setting_type, self.NON_EDITABLE)
field = deepcopy(field) field = deepcopy(field)
value = initial['value']
default = initial['default']
real_field = field real_field = field
while isinstance(real_field, partial): while isinstance(real_field, partial):
real_field = real_field.func real_field = real_field.func
# Do not serialize following form types # 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): if real_field not in (forms.MultipleChoiceField, forms.BooleanField):
value = serialized_value value = serialized_value
if real_field is not forms.BooleanField:
default = serialized_default default = serialized_default
initial['value'] = value initial['value'] = value
initial['default'] = default initial['default'] = default
super(SettingForm, self).__init__(*args, **kwargs) super(SettingForm, self).__init__(*args, **kwargs)
if initial: if initial:
self.changed = bool(value != default)
self.fields['value'] = field(label=_("value")) self.fields['value'] = field(label=_("value"))
if isinstance(self.fields['value'].widget, forms.Textarea): if isinstance(self.fields['value'].widget, forms.Textarea):
rows = math.ceil(len(value)/65) rows = math.ceil(len(value)/65)
self.fields['value'].widget.attrs['rows'] = rows 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.fields['name'].widget.attrs['readonly'] = True
self.app = initial['app'] self.app = initial['app']
@ -101,11 +102,10 @@ class SettingForm(ReadOnlyFormMixin, forms.Form):
value = eval(value, parser.get_eval_context()) value = eval(value, parser.get_eval_context())
except Exception as exc: except Exception as exc:
raise ValidationError(str(exc)) raise ValidationError(str(exc))
self.setting.validate_value(value)
if not isinstance(value, self.setting_type): if not isinstance(value, self.setting_type):
if self.setting_type in (tuple, list) and isinstance(value, (tuple, list)): if self.setting_type in (tuple, list) and isinstance(value, (tuple, list)):
value = self.setting_type(value) value = self.setting_type(value)
else:
raise ValidationError("Please provide a %s." % self.setting_type.__name__)
return value return value

View File

@ -17,8 +17,15 @@
</div> </div>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<ul class="object-tools">
{% block object-tools-items %}
<li>
<a href="./view/" class="historylink">{% trans "View file" %}</a>
</li>
{% endblock %}
</ul>
<div> <div>
<form method="post" action="">{% csrf_token %} <form method="post" action="">{% csrf_token %}
{% if diff %} {% if diff %}
@ -69,7 +76,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{{ field.errors.as_ul }} {{ 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> <p class="help" style="max-width:100px; white-space:nowrap;">{{ field.help_text }}</p>
</td> </td>
{% endfor %} {% endfor %}
@ -82,5 +89,3 @@
{% endif %} {% endif %}
</form> </form>
{% endblock %} {% 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 from ..utils.python import import_class
def all_valid(kwargs): def all_valid(*args):
""" helper function to merge multiple validators at once """ """ helper function to merge multiple validators at once """
if len(args) == 1:
# Dict
errors = {} errors = {}
kwargs = args
for field, validator in kwargs.items(): for field, validator in kwargs.items():
try: try:
validator[0](*validator[1:]) validator[0](*validator[1:])
except ValidationError as error: except ValidationError as error:
errors[field] = 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: if errors:
raise ValidationError(errors) raise ValidationError(errors)

View File

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

View File

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

View File

@ -1,8 +1,11 @@
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .core import validators
class Setting(object): class Setting(object):
""" """
@ -20,11 +23,12 @@ class Setting(object):
value = ("'%s'" if isinstance(value, str) else '%s') % value value = ("'%s'" if isinstance(value, str) else '%s') % value
return '<%s: %s>' % (self.name, 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: if call_init:
return super(Setting, cls).__new__(cls) return super(Setting, cls).__new__(cls)
cls.settings[name] = cls(name, default, help_text=help_text, choices=choices, 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) return cls.get_value(name, default)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -32,22 +36,31 @@ class Setting(object):
for name, value in kwargs.items(): for name, value in kwargs.items():
setattr(self, name, value) setattr(self, name, value)
self.value = self.get_value(self.name, self.default) self.value = self.get_value(self.name, self.default)
self.validate_value(self.value)
self.settings[name] = self 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 @classmethod
def get_value(cls, name, default): def get_value(cls, name, default):
return getattr(cls.conf_settings, name, default) return getattr(cls.conf_settings, name, default)
# TODO validation, defaults to same type
ORCHESTRA_BASE_DOMAIN = Setting('ORCHESTRA_BASE_DOMAIN', ORCHESTRA_BASE_DOMAIN = Setting('ORCHESTRA_BASE_DOMAIN',
'orchestra.lan' '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." help_text=_("Domain name used when it will not be possible to infere the domain from a request."
"For example in periodic tasks.") "For example in periodic tasks.")
) )