Improvements settings admin management
This commit is contained in:
parent
759c01c64c
commit
929d9beb5c
12
TODO.md
12
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):
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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 """
|
||||
|
|
|
@ -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')"
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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">†</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 %}
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
› <a href="../">Settings</a>
|
||||
› View file content
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
{% blocktrans %}
|
||||
<p>Current <tt>{{ settings_file }}</tt> content.</p>
|
||||
{% endblocktrans %}
|
||||
<PRE>{{ content }}</PRE>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:]))
|
||||
|
|
|
@ -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.")
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue