Add (admin-friendly!) view to manage some App Settings
which settings are available to this view will depend on the project's settings module, similar to how the email settings work
This commit is contained in:
parent
012a06d8a6
commit
4e09b757c3
29
tailbone/static/js/tailbone.appsettings.js
Normal file
29
tailbone/static/js/tailbone.appsettings.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
/************************************************************
|
||||
*
|
||||
* tailbone.appsettings.js
|
||||
*
|
||||
* Logic for App Settings page.
|
||||
*
|
||||
************************************************************/
|
||||
|
||||
|
||||
function show_group(group) {
|
||||
if (group == "(All)") {
|
||||
$('.panel').show();
|
||||
} else {
|
||||
$('.panel').hide();
|
||||
$('.panel[data-groupname="' + group + '"]').show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$(function() {
|
||||
|
||||
$('#settings-group').on('selectmenuchange', function(event, ui) {
|
||||
show_group(ui.item.value);
|
||||
});
|
||||
|
||||
show_group($('#settings-group').val());
|
||||
|
||||
});
|
99
tailbone/templates/appsettings.mako
Normal file
99
tailbone/templates/appsettings.mako
Normal file
|
@ -0,0 +1,99 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/base.mako" />
|
||||
|
||||
<%def name="title()">${self.app_title()} App Settings</%def>
|
||||
|
||||
<%def name="content_title()"></%def>
|
||||
|
||||
<%def name="extra_javascript()">
|
||||
${parent.extra_javascript()}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.appsettings.js') + '?ver={}'.format(tailbone.__version__))}
|
||||
</%def>
|
||||
|
||||
<%def name="extra_styles()">
|
||||
${parent.extra_styles()}
|
||||
<style type="text/css">
|
||||
div.form {
|
||||
float: none;
|
||||
}
|
||||
div.panel {
|
||||
width: 85%;
|
||||
}
|
||||
.field-wrapper {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
.panel .field-wrapper label {
|
||||
font-family: monospace;
|
||||
width: 50em;
|
||||
}
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<div class="form">
|
||||
${h.form(form.action_url, id=dform.formid, method='post', class_='autodisable')}
|
||||
${h.csrf_token(request)}
|
||||
|
||||
% if dform.error:
|
||||
<div class="error-messages">
|
||||
<div class="ui-state-error ui-corner-all">
|
||||
<span style="float: left; margin-right: .3em;" class="ui-icon ui-icon-alert"></span>
|
||||
Please see errors below.
|
||||
</div>
|
||||
<div class="ui-state-error ui-corner-all">
|
||||
<span style="float: left; margin-right: .3em;" class="ui-icon ui-icon-alert"></span>
|
||||
${dform.error}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="group-picker">
|
||||
<div class="field-wrapper">
|
||||
<label for="settings-group">Showing Group</label>
|
||||
<div class="field">
|
||||
${h.select('settings-group', current_group, group_options, **{'auto-enhance': 'true'})}
|
||||
## ${h.select('settings-group', current_group, group_options)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
% for group in groups:
|
||||
<div class="panel" data-groupname="${group}">
|
||||
<h2>${group}</h2>
|
||||
<div class="panel-body">
|
||||
|
||||
% for setting in settings:
|
||||
% if setting.group == group:
|
||||
<% field = dform[setting.node_name] %>
|
||||
|
||||
<div class="field-wrapper ${field.name} ${'with-error' if field.error else ''}">
|
||||
% if field.error:
|
||||
<div class="field-error">
|
||||
% for msg in field.error.messages():
|
||||
<span class="error-msg">${msg}</span>
|
||||
% endfor
|
||||
</div>
|
||||
% endif
|
||||
<div class="field-row">
|
||||
<label for="${field.oid}">${form.get_label(field.name)}</label>
|
||||
<div class="field">
|
||||
${field.serialize()|n}
|
||||
</div>
|
||||
</div>
|
||||
% if form.has_helptext(field.name):
|
||||
<span class="instructions">${form.render_helptext(field.name)}</span>
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
</div><!-- panel-body -->
|
||||
</div><! -- panel -->
|
||||
% endfor
|
||||
|
||||
<div class="buttons">
|
||||
${h.submit('save', getattr(form, 'submit_label', getattr(form, 'save_label', "Submit")))}
|
||||
${h.link_to("Cancel", form.cancel_url, class_='cancel button{}'.format(' autodisable' if form.auto_disable_cancel else ''))}
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
</div>
|
|
@ -51,6 +51,9 @@
|
|||
${grid_index_nav()}
|
||||
% endif
|
||||
% endif
|
||||
% elif index_title:
|
||||
<span class="global">»</span>
|
||||
<span class="global">${index_title}</span>
|
||||
% endif
|
||||
|
||||
<div class="feedback">
|
||||
|
|
|
@ -39,7 +39,7 @@ ${h.csrf_token(request)}
|
|||
</div>
|
||||
% endif
|
||||
<div class="field-row">
|
||||
<label for="${field.oid}">${field.title}</label>
|
||||
<label for="${field.oid}">${form.get_label(field.name)}</label>
|
||||
<div class="field">
|
||||
${field.serialize()|n}
|
||||
</div>
|
||||
|
|
|
@ -28,11 +28,18 @@ from __future__ import unicode_literals, absolute_import
|
|||
|
||||
import re
|
||||
|
||||
from rattail.db import model
|
||||
import six
|
||||
|
||||
from rattail.db import model, api
|
||||
from rattail.settings import Setting
|
||||
from rattail.util import import_module_path
|
||||
|
||||
import colander
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from tailbone.views import MasterView
|
||||
from tailbone import forms
|
||||
from tailbone.db import Session
|
||||
from tailbone.views import MasterView, View
|
||||
|
||||
|
||||
class SettingsView(MasterView):
|
||||
|
@ -77,5 +84,116 @@ class SettingsView(MasterView):
|
|||
return True
|
||||
|
||||
|
||||
class AppSettingsForm(forms.Form):
|
||||
|
||||
def get_label(self, key):
|
||||
return self.labels.get(key, key)
|
||||
|
||||
|
||||
class AppSettingsView(View):
|
||||
"""
|
||||
Core view which exposes "app settings" - aka. admin-friendly settings with
|
||||
descriptions and type-specific form controls etc.
|
||||
"""
|
||||
|
||||
def __call__(self):
|
||||
settings = sorted(self.iter_known_settings(),
|
||||
key=lambda setting: (setting.group,
|
||||
setting.namespace,
|
||||
setting.name))
|
||||
groups = sorted(set([setting.group for setting in settings]))
|
||||
current_group = None
|
||||
|
||||
form = self.make_form(settings)
|
||||
form.cancel_url = self.request.current_route_url()
|
||||
if form.validate(newstyle=True):
|
||||
self.save_form(form)
|
||||
group = self.request.POST.get('settings-group')
|
||||
if group:
|
||||
self.request.session['appsettings.current_group'] = group
|
||||
self.request.session.flash("App Settings have been saved.")
|
||||
return self.redirect(self.request.current_route_url())
|
||||
|
||||
if self.request.method == 'POST':
|
||||
current_group = self.request.POST.get('settings-group')
|
||||
|
||||
if not current_group:
|
||||
current_group = self.request.session.get('appsettings.current_group')
|
||||
|
||||
group_options = [tags.Option(group, group) for group in groups]
|
||||
group_options.insert(0, tags.Option("(All)", "(All)"))
|
||||
return {
|
||||
'index_title': "App Settings",
|
||||
'form': form,
|
||||
'dform': form.make_deform_form(),
|
||||
'groups': groups,
|
||||
'group_options': group_options,
|
||||
'current_group': current_group,
|
||||
'settings': settings,
|
||||
}
|
||||
|
||||
def make_form(self, known_settings):
|
||||
schema = colander.MappingSchema()
|
||||
helptext = {}
|
||||
for setting in known_settings:
|
||||
kwargs = {
|
||||
'name': setting.node_name,
|
||||
'default': self.get_setting_value(setting),
|
||||
}
|
||||
if setting.choices:
|
||||
kwargs['validator'] = colander.OneOf(setting.choices)
|
||||
kwargs['widget'] = forms.widgets.JQuerySelectWidget(
|
||||
values=[(val, val) for val in setting.choices])
|
||||
schema.add(colander.SchemaNode(self.get_node_type(setting), **kwargs))
|
||||
helptext[setting.node_name] = setting.__doc__.strip()
|
||||
return AppSettingsForm(schema=schema, request=self.request, helptext=helptext)
|
||||
|
||||
def get_node_type(self, setting):
|
||||
if setting.data_type is bool:
|
||||
return colander.Bool()
|
||||
return colander.String()
|
||||
|
||||
def save_form(self, form):
|
||||
for setting in self.iter_known_settings():
|
||||
value = form.validated[setting.node_name]
|
||||
self.save_setting_value(setting, value)
|
||||
|
||||
def iter_known_settings(self):
|
||||
"""
|
||||
Iterate over all known settings.
|
||||
"""
|
||||
for module in self.rattail_config.getlist('rattail', 'settings', default=['rattail.settings']):
|
||||
module = import_module_path(module)
|
||||
for name in dir(module):
|
||||
obj = getattr(module, name)
|
||||
if isinstance(obj, type) and issubclass(obj, Setting) and obj is not Setting:
|
||||
# NOTE: we set this here, and reference it elsewhere
|
||||
obj.node_name = self.get_node_name(obj)
|
||||
yield obj
|
||||
|
||||
def get_node_name(self, setting):
|
||||
return '[{}] {}'.format(setting.namespace, setting.name)
|
||||
|
||||
def get_setting_value(self, setting):
|
||||
if setting.data_type is bool:
|
||||
return self.rattail_config.getbool(setting.namespace, setting.name)
|
||||
return self.rattail_config.get(setting.namespace, setting.name, default='')
|
||||
|
||||
def save_setting_value(self, setting, value):
|
||||
legacy_name = '{}.{}'.format(setting.namespace, setting.name)
|
||||
if setting.data_type is bool:
|
||||
api.save_setting(Session(), legacy_name, 'true' if value else 'false')
|
||||
else:
|
||||
api.save_setting(Session(), legacy_name, six.text_type(value))
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
config.add_route('appsettings', '/settings/app/')
|
||||
config.add_view(cls, route_name='appsettings',
|
||||
renderer='/appsettings.mako',
|
||||
permission='settings.edit')
|
||||
|
||||
|
||||
def includeme(config):
|
||||
AppSettingsView.defaults(config)
|
||||
SettingsView.defaults(config)
|
||||
|
|
Loading…
Reference in a new issue