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 name="content_title()"> + +<%def name="extra_javascript()"> + ${parent.extra_javascript()} + ${h.javascript_link(request.static_url('tailbone:static/js/tailbone.appsettings.js') + '?ver={}'.format(tailbone.__version__))} + + +<%def name="extra_styles()"> + ${parent.extra_styles()} + + + +
+ ${h.form(form.action_url, id=dform.formid, method='post', class_='autodisable')} + ${h.csrf_token(request)} + + % if dform.error: +
+
+ + Please see errors below. +
+
+ + ${dform.error} +
+
+ % endif + +
+
+ +
+ ${h.select('settings-group', current_group, group_options, **{'auto-enhance': 'true'})} + ## ${h.select('settings-group', current_group, group_options)} +
+
+
+ + % for group in groups: +
+

${group}

+
+ + % for setting in settings: + % if setting.group == group: + <% field = dform[setting.node_name] %> + +
+ % if field.error: +
+ % for msg in field.error.messages(): + ${msg} + % endfor +
+ % endif +
+ +
+ ${field.serialize()|n} +
+
+ % if form.has_helptext(field.name): + ${form.render_helptext(field.name)} + % endif +
+ % endif + % endfor + +
+
+ % endfor + +
+ ${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 ''))} +
+ + ${h.end_form()} +
diff --git a/tailbone/templates/base.mako b/tailbone/templates/base.mako index cf348b3f..f4a18475 100644 --- a/tailbone/templates/base.mako +++ b/tailbone/templates/base.mako @@ -51,6 +51,9 @@ ${grid_index_nav()} % endif % endif + % elif index_title: + » + ${index_title} % endif
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)