diff --git a/tailbone/static/js/tailbone.appsettings.js b/tailbone/static/js/tailbone.appsettings.js
new file mode 100644
index 00000000..ae378931
--- /dev/null
+++ b/tailbone/static/js/tailbone.appsettings.js
@@ -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());
+
+});
diff --git a/tailbone/templates/appsettings.mako b/tailbone/templates/appsettings.mako
new file mode 100644
index 00000000..b52d3b5c
--- /dev/null
+++ b/tailbone/templates/appsettings.mako
@@ -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()}
+
+%def>
+
+
diff --git a/tailbone/templates/forms/deform.mako b/tailbone/templates/forms/deform.mako
index 36d53644..b3163557 100644
--- a/tailbone/templates/forms/deform.mako
+++ b/tailbone/templates/forms/deform.mako
@@ -39,7 +39,7 @@ ${h.csrf_token(request)}
% endif
-
+
${field.serialize()|n}
diff --git a/tailbone/views/settings.py b/tailbone/views/settings.py
index 4bc135a7..ee412bac 100644
--- a/tailbone/views/settings.py
+++ b/tailbone/views/settings.py
@@ -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)