From 929d9beb5c615889aa79403cee8d557701950160 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Sun, 26 Apr 2015 15:52:40 +0000 Subject: [PATCH] Improvements settings admin management --- TODO.md | 12 ------- orchestra/admin/menu.py | 2 +- orchestra/contrib/bills/admin.py | 4 +-- orchestra/contrib/domains/settings.py | 8 ++--- orchestra/contrib/saas/settings.py | 4 +-- orchestra/contrib/settings/admin.py | 31 ++++++++++++++----- orchestra/contrib/settings/forms.py | 20 ++++++------ .../templates/admin/settings/change_form.html | 13 +++++--- .../templates/admin/settings/view.html | 29 +++++++++++++++++ orchestra/core/validators.py | 26 +++++++++++----- orchestra/forms/options.py | 4 +-- orchestra/forms/widgets.py | 5 ++- orchestra/settings.py | 25 +++++++++++---- 13 files changed, 122 insertions(+), 61 deletions(-) create mode 100644 orchestra/contrib/settings/templates/admin/settings/view.html diff --git a/TODO.md b/TODO.md index b470c465..f72179e1 100644 --- a/TODO.md +++ b/TODO.md @@ -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): - diff --git a/orchestra/admin/menu.py b/orchestra/admin/menu.py index bb1e1c75..93b11000 100644 --- a/orchestra/admin/menu.py +++ b/orchestra/admin/menu.py @@ -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') diff --git a/orchestra/contrib/bills/admin.py b/orchestra/contrib/bills/admin.py index 648940a1..478d7db8 100644 --- a/orchestra/contrib/bills/admin.py +++ b/orchestra/contrib/bills/admin.py @@ -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 """ diff --git a/orchestra/contrib/domains/settings.py b/orchestra/contrib/domains/settings.py index 4d112655..2c20a7b9 100644 --- a/orchestra/contrib/domains/settings.py +++ b/orchestra/contrib/domains/settings.py @@ -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
" + "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
" "'%(site_dir)s/forbidden_domains.list')" ) ) diff --git a/orchestra/contrib/saas/settings.py b/orchestra/contrib/saas/settings.py index 79221eee..6bc2ba0f 100644 --- a/orchestra/contrib/saas/settings.py +++ b/orchestra/contrib/saas/settings.py @@ -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) ) diff --git a/orchestra/contrib/settings/admin.py b/orchestra/contrib/settings/admin.py index 52785c0d..9fd3edbf 100644 --- a/orchestra/contrib/settings/admin.py +++ b/orchestra/contrib/settings/admin.py @@ -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') + diff --git a/orchestra/contrib/settings/forms.py b/orchestra/contrib/settings/forms.py index c5110f1d..515087db 100644 --- a/orchestra/contrib/settings/forms.py +++ b/orchestra/contrib/settings/forms.py @@ -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 diff --git a/orchestra/contrib/settings/templates/admin/settings/change_form.html b/orchestra/contrib/settings/templates/admin/settings/change_form.html index ed935ad6..930ee8cc 100644 --- a/orchestra/contrib/settings/templates/admin/settings/change_form.html +++ b/orchestra/contrib/settings/templates/admin/settings/change_form.html @@ -17,8 +17,15 @@ {% endblock %} - {% block content %} + +
{% csrf_token %} {% if diff %} @@ -69,7 +76,7 @@ {% endfor %} {% endif %} {{ field.errors.as_ul }} -
{{ field }}{% if forloop.last %}{% if form.changed %}
{% endif %}{% endif %}
+
{{ field }}{% if forloop.last %}{% if form.changed %}
*
{% endif %}{% endif %}

{{ field.help_text }}

{% endfor %} @@ -82,5 +89,3 @@ {% endif %}
{% endblock %} - - diff --git a/orchestra/contrib/settings/templates/admin/settings/view.html b/orchestra/contrib/settings/templates/admin/settings/view.html new file mode 100644 index 00000000..86c73255 --- /dev/null +++ b/orchestra/contrib/settings/templates/admin/settings/view.html @@ -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 }} + + +{% endblock %} + + +{% block breadcrumbs %} + +{% endblock %} + + +{% block content %} +
+ {% blocktrans %} +

Current {{ settings_file }} content.

+ {% endblocktrans %} +
{{ content }}
+
+{% endblock %} diff --git a/orchestra/core/validators.py b/orchestra/core/validators.py index 5a90564e..8c330d90 100644 --- a/orchestra/core/validators.py +++ b/orchestra/core/validators.py @@ -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) diff --git a/orchestra/forms/options.py b/orchestra/forms/options.py index 0404275d..41beb62f 100644 --- a/orchestra/forms/options.py +++ b/orchestra/forms/options.py @@ -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 diff --git a/orchestra/forms/widgets.py b/orchestra/forms/widgets.py index 63317a27..f275e0f3 100644 --- a/orchestra/forms/widgets.py +++ b/orchestra/forms/widgets.py @@ -13,7 +13,6 @@ class SpanWidget(forms.Widget): Renders a value wrapped in a tag. Requires use of specific form support. (see ReadonlyForm or ReadonlyModelForm) """ - def __init__(self, *args, **kwargs): self.tag = kwargs.pop('tag', '') 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('%s' % (icon, str(original_value))) tag = self.tag[:-1] endtag = '/'.join((self.tag[0], self.tag[1:])) diff --git a/orchestra/settings.py b/orchestra/settings.py index 477c26b2..055fdab0 100644 --- a/orchestra/settings.py +++ b/orchestra/settings.py @@ -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.") )