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:
Lance Edgar 2018-07-18 13:09:32 -05:00
parent 012a06d8a6
commit 4e09b757c3
5 changed files with 252 additions and 3 deletions

View 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());
});

View 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>

View file

@ -51,6 +51,9 @@
${grid_index_nav()}
% endif
% endif
% elif index_title:
<span class="global">&raquo;</span>
<span class="global">${index_title}</span>
% endif
<div class="feedback">

View file

@ -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>

View file

@ -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)