719e7c8441
such names should never use plural forms. for now what plural forms were previously in use, should still work. ideally can remove those at some point
274 lines
9.7 KiB
Python
274 lines
9.7 KiB
Python
# -*- coding: utf-8; -*-
|
|
################################################################################
|
|
#
|
|
# Rattail -- Retail Software Framework
|
|
# Copyright © 2010-2021 Lance Edgar
|
|
#
|
|
# This file is part of Rattail.
|
|
#
|
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
|
# terms of the GNU General Public License as published by the Free Software
|
|
# Foundation, either version 3 of the License, or (at your option) any later
|
|
# version.
|
|
#
|
|
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along with
|
|
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
################################################################################
|
|
"""
|
|
Settings Views
|
|
"""
|
|
|
|
from __future__ import unicode_literals, absolute_import
|
|
|
|
import re
|
|
|
|
import six
|
|
|
|
from rattail.db import model, api
|
|
from rattail.settings import Setting
|
|
from rattail.util import import_module_path
|
|
from rattail.config import parse_bool
|
|
|
|
import colander
|
|
from webhelpers2.html import tags
|
|
|
|
from tailbone import forms
|
|
from tailbone.db import Session
|
|
from tailbone.views import MasterView, View
|
|
|
|
|
|
class SettingView(MasterView):
|
|
"""
|
|
Master view for the settings model.
|
|
"""
|
|
model_class = model.Setting
|
|
model_title = "Raw Setting"
|
|
model_title_plural = "Raw Settings"
|
|
bulk_deletable = True
|
|
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
|
|
|
|
grid_columns = [
|
|
'name',
|
|
'value',
|
|
]
|
|
|
|
def configure_grid(self, g):
|
|
super(SettingView, self).configure_grid(g)
|
|
g.filters['name'].default_active = True
|
|
g.filters['name'].default_verb = 'contains'
|
|
g.set_sort_defaults('name')
|
|
g.set_link('name')
|
|
|
|
def configure_form(self, f):
|
|
super(SettingView, self).configure_form(f)
|
|
if self.creating:
|
|
f.set_validator('name', self.unique_name)
|
|
|
|
def unique_name(self, node, value):
|
|
setting = self.Session.query(model.Setting).get(value)
|
|
if setting:
|
|
raise colander.Invalid(node, "Setting name must be unique")
|
|
|
|
def editable_instance(self, setting):
|
|
if self.rattail_config.demo():
|
|
return not bool(self.feedback.match(setting.name))
|
|
return True
|
|
|
|
def deletable_instance(self, setting):
|
|
if self.rattail_config.demo():
|
|
return not bool(self.feedback.match(setting.name))
|
|
return True
|
|
|
|
# TODO: deprecate / remove this
|
|
SettingsView = SettingView
|
|
|
|
|
|
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 is not None:
|
|
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')
|
|
|
|
use_buefy = self.get_use_buefy()
|
|
context = {
|
|
'index_title': "App Settings",
|
|
'form': form,
|
|
'dform': form.make_deform_form(),
|
|
'groups': groups,
|
|
'settings': settings,
|
|
'use_buefy': use_buefy,
|
|
}
|
|
if use_buefy:
|
|
context['buefy_data'] = self.get_buefy_data(form, groups, settings)
|
|
# TODO: this seems hacky, and probably only needed if theme changes?
|
|
if current_group == '(All)':
|
|
current_group = ''
|
|
else:
|
|
group_options = [tags.Option(group, group) for group in groups]
|
|
group_options.insert(0, tags.Option("(All)", "(All)"))
|
|
context['group_options'] = group_options
|
|
context['current_group'] = current_group
|
|
return context
|
|
|
|
def get_buefy_data(self, form, groups, settings):
|
|
dform = form.make_deform_form()
|
|
grouped = dict([(label, [])
|
|
for label in groups])
|
|
|
|
for setting in settings:
|
|
field = dform[setting.node_name]
|
|
s = {
|
|
'field_name': field.name,
|
|
'label': form.get_label(field.name),
|
|
'data_type': setting.data_type.__name__,
|
|
'choices': setting.choices,
|
|
'helptext': form.render_helptext(field.name) if form.has_helptext(field.name) else None,
|
|
'error': field.error,
|
|
}
|
|
value = self.get_setting_value(setting)
|
|
if setting.data_type is bool:
|
|
value = parse_bool(value)
|
|
s['value'] = value
|
|
if field.error:
|
|
s['error_messages'] = field.error_messages()
|
|
grouped[setting.group].append(s)
|
|
|
|
data = []
|
|
for label in groups:
|
|
group = {'label': label, 'settings': grouped[label]}
|
|
data.append(group)
|
|
|
|
return data
|
|
|
|
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 kwargs['default'] is None or kwargs['default'] == '':
|
|
kwargs['default'] = colander.null
|
|
if not setting.required:
|
|
kwargs['missing'] = colander.null
|
|
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()
|
|
elif setting.data_type is int:
|
|
return colander.Integer()
|
|
return colander.String()
|
|
|
|
def save_form(self, form):
|
|
for setting in self.iter_known_settings():
|
|
value = form.validated[setting.node_name]
|
|
if value is colander.null:
|
|
value = ''
|
|
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)
|
|
if setting.data_type is list:
|
|
return '\n'.join(
|
|
self.rattail_config.getlist(setting.namespace, setting.name,
|
|
default=[]))
|
|
return self.rattail_config.get(setting.namespace, setting.name)
|
|
|
|
def save_setting_value(self, setting, value):
|
|
existing = self.get_setting_value(setting)
|
|
if existing != value:
|
|
legacy_name = '{}.{}'.format(setting.namespace, setting.name)
|
|
if setting.data_type is bool:
|
|
value = 'true' if value else 'false'
|
|
elif setting.data_type is list:
|
|
entries = [self.clean_list_entry(entry)
|
|
for entry in value.split('\n')]
|
|
value = ', '.join(entries)
|
|
else:
|
|
value = six.text_type(value)
|
|
api.save_setting(Session(), legacy_name, value)
|
|
|
|
def clean_list_entry(self, value):
|
|
value = value.strip()
|
|
if '"' in value and "'" in value:
|
|
raise NotImplementedError("don't know how to handle escaping 2 "
|
|
"different types of quotes!")
|
|
if '"' in value:
|
|
return "'{}'".format(value)
|
|
if "'" in value:
|
|
return '"{}"'.format(value)
|
|
return 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)
|
|
SettingView.defaults(config)
|