diff --git a/tailbone/templates/configure.mako b/tailbone/templates/configure.mako
index de2b4e78..f05b24c0 100644
--- a/tailbone/templates/configure.mako
+++ b/tailbone/templates/configure.mako
@@ -30,14 +30,24 @@
%def>
+<%def name="intro_message()">
+
+ This page lets you modify the
+ % if config_preferences is not Undefined and config_preferences:
+ preferences
+ % else:
+ configuration
+ % endif
+ for ${config_title}.
+
+%def>
+
<%def name="buttons_row()">
-
- This page lets you modify the configuration for ${config_title}.
-
+ ${self.intro_message()}
diff --git a/tailbone/templates/themes/falafel/base.mako b/tailbone/templates/themes/falafel/base.mako
index 94e20f3e..db669c78 100644
--- a/tailbone/templates/themes/falafel/base.mako
+++ b/tailbone/templates/themes/falafel/base.mako
@@ -549,6 +549,7 @@
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
% endif
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
+ ${h.link_to("Edit Preferences", url('my.preferences'), class_='navbar-item')}
${h.link_to("Logout", url('logout'), class_='navbar-item')}
diff --git a/tailbone/templates/users/preferences.mako b/tailbone/templates/users/preferences.mako
new file mode 100644
index 00000000..a44534dc
--- /dev/null
+++ b/tailbone/templates/users/preferences.mako
@@ -0,0 +1,55 @@
+## -*- coding: utf-8; -*-
+<%inherit file="/configure.mako" />
+
+<%def name="title()">
+ % if current_user:
+ Edit Preferences
+ % else:
+ ${index_title} » ${instance_title} » Preferences
+ % endif
+%def>
+
+<%def name="content_title()">Preferences%def>
+
+<%def name="intro_message()">
+
+ % if current_user:
+ This page lets you modify your preferences.
+ % else:
+ This page lets you modify the preferences for ${config_title}.
+ % endif
+
+%def>
+
+<%def name="form_content()">
+
+
Display
+
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
+
+%def>
+
+<%def name="modify_this_page_vars()">
+ ${parent.modify_this_page_vars()}
+
+%def>
+
+
+${parent.body()}
diff --git a/tailbone/templates/users/view.mako b/tailbone/templates/users/view.mako
index 8477ebfa..b34902a1 100644
--- a/tailbone/templates/users/view.mako
+++ b/tailbone/templates/users/view.mako
@@ -14,4 +14,12 @@
% endif
%def>
+<%def name="context_menu_items()">
+ ${parent.context_menu_items()}
+ % if master.has_perm('preferences'):
+
${h.link_to("Edit User Preferences", action_url('preferences', instance))}
+ % endif
+%def>
+
+
${parent.body()}
diff --git a/tailbone/views/master.py b/tailbone/views/master.py
index 77a844a4..89def384 100644
--- a/tailbone/views/master.py
+++ b/tailbone/views/master.py
@@ -4290,6 +4290,7 @@ class MasterView(View):
'type': bool,
'value': config.getbool('rattail.batch',
'purchase.allow_cases'),
+ 'save_if_empty': False,
}
Note that some of the above is optional, in particular it
@@ -4316,9 +4317,11 @@ class MasterView(View):
return '{}.{}'.format(simple['section'],
simple['option'])
- def configure_get_context(self):
+ def configure_get_context(self, simple_settings=None,
+ input_file_templates=True):
context = {}
- simple_settings = self.configure_get_simple_settings()
+ if simple_settings is None:
+ simple_settings = self.configure_get_simple_settings()
if simple_settings:
config = self.rattail_config
@@ -4342,7 +4345,7 @@ class MasterView(View):
context['simple_settings'] = settings
# add settings for downloadable input file templates, if any
- if self.has_input_file_templates:
+ if input_file_templates and self.has_input_file_templates:
settings = {}
file_options = {}
file_option_dirs = {}
@@ -4359,11 +4362,13 @@ class MasterView(View):
return context
- def configure_gather_settings(self, data):
+ def configure_gather_settings(self, data, simple_settings=None,
+ input_file_templates=True):
settings = []
# maybe collect "simple" settings
- simple_settings = self.configure_get_simple_settings()
+ if simple_settings is None:
+ simple_settings = self.configure_get_simple_settings()
if simple_settings:
for simple in simple_settings:
@@ -4377,11 +4382,14 @@ class MasterView(View):
else:
value = six.text_type(value)
- settings.append({'name': name,
- 'value': value})
+ # only want to save this setting if we received a
+ # value, or if empty values are okay to save
+ if value or simple.get('save_if_empty'):
+ settings.append({'name': name,
+ 'value': value})
# maybe also collect input file template settings
- if self.has_input_file_templates:
+ if input_file_templates and self.has_input_file_templates:
for template in self.normalize_input_file_templates():
# mode
@@ -4401,16 +4409,18 @@ class MasterView(View):
return settings
- def configure_remove_settings(self):
+ def configure_remove_settings(self, simple_settings=None,
+ input_file_templates=True):
model = self.model
names = []
- simple_settings = self.configure_get_simple_settings()
+ if simple_settings is None:
+ simple_settings = self.configure_get_simple_settings()
if simple_settings:
names.extend([self.configure_get_name_for_simple_setting(simple)
for simple in simple_settings])
- if self.has_input_file_templates:
+ if input_file_templates and self.has_input_file_templates:
for template in self.normalize_input_file_templates():
names.extend([
template['setting_mode'],
diff --git a/tailbone/views/users.py b/tailbone/views/users.py
index 52064346..ecff3bb9 100644
--- a/tailbone/views/users.py
+++ b/tailbone/views/users.py
@@ -26,12 +26,10 @@ User Views
from __future__ import unicode_literals, absolute_import
-import copy
-
import six
from sqlalchemy import orm
-from rattail.db import model
+from rattail.db.model import User, UserEvent
from rattail.db.auth import (administrator_role, guest_role,
authenticated_role, set_user_password)
@@ -40,8 +38,7 @@ from deform import widget as dfwidget
from webhelpers2.html import HTML, tags
from tailbone import forms
-from tailbone.db import Session
-from tailbone.views import MasterView
+from tailbone.views import MasterView, View
from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer
@@ -49,9 +46,9 @@ class UserView(PrincipalMasterView):
"""
Master view for the User model.
"""
- model_class = model.User
+ model_class = User
has_rows = True
- model_row_class = model.UserEvent
+ model_row_class = UserEvent
has_versions = True
touchable = True
@@ -99,6 +96,7 @@ class UserView(PrincipalMasterView):
def query(self, session):
query = super(UserView, self).query(session)
+ model = self.model
# bring in the related Person(s)
query = query.outerjoin(model.Person)\
@@ -108,6 +106,7 @@ class UserView(PrincipalMasterView):
def configure_grid(self, g):
super(UserView, self).configure_grid(g)
+ model = self.model
del g.filters['salt']
g.filters['username'].default_active = True
@@ -160,6 +159,7 @@ class UserView(PrincipalMasterView):
return not self.user_is_protected(user)
def unique_username(self, node, value):
+ model = self.model
query = self.Session.query(model.User)\
.filter(model.User.username == value)
if self.editing:
@@ -181,6 +181,7 @@ class UserView(PrincipalMasterView):
def configure_form(self, f):
super(UserView, self).configure_form(f)
+ model = self.model
user = f.model_instance
# username
@@ -265,6 +266,7 @@ class UserView(PrincipalMasterView):
f.remove('set_password')
def get_possible_roles(self):
+ model = self.model
# some roles should never have users "belong" to them
excluded = [
@@ -281,6 +283,7 @@ class UserView(PrincipalMasterView):
.order_by(model.Role.name)
def objectify(self, form, data=None):
+ model = self.model
# create/update user as per normal
if data is None:
@@ -328,6 +331,7 @@ class UserView(PrincipalMasterView):
if 'roles' not in data:
return
+ model = self.model
old_roles = set([r.uuid for r in user.roles])
new_roles = data['roles']
admin = administrator_role(self.Session())
@@ -389,6 +393,7 @@ class UserView(PrincipalMasterView):
return HTML.tag('ul', c=items)
def get_row_data(self, user):
+ model = self.model
return self.Session.query(model.UserEvent)\
.filter(model.UserEvent.user == user)
@@ -402,6 +407,7 @@ class UserView(PrincipalMasterView):
g.main_actions = []
def get_version_child_classes(self):
+ model = self.model
return [
(model.UserRole, 'user_uuid'),
]
@@ -409,6 +415,7 @@ class UserView(PrincipalMasterView):
def find_principals_with_permission(self, session, permission):
app = self.get_rattail_app()
auth = app.get_auth_handler()
+ model = self.model
# TODO: this should search Permission table instead, and work backward to User?
all_users = session.query(model.User)\
@@ -448,6 +455,105 @@ class UserView(PrincipalMasterView):
assert not removing._roles
self.Session.delete(removing)
+ def preferences(self, user=None):
+ """
+ View to modify preferences for a particular user.
+ """
+ current_user = True
+ if not user:
+ current_user = False
+ user = self.get_instance()
+
+ # TODO: this is of course largely copy/pasted from the
+ # MasterView.configure() method..should refactor?
+ if self.request.method == 'POST':
+ if self.request.POST.get('remove_settings'):
+ self.preferences_remove_settings(user)
+ self.request.session.flash("Settings have been removed.")
+ return self.redirect(self.request.current_route_url())
+ else:
+ data = self.request.POST
+
+ # then gather/save settings
+ settings = self.preferences_gather_settings(data, user)
+ self.preferences_remove_settings(user)
+ self.configure_save_settings(settings)
+ self.request.session.flash("Settings have been saved.")
+ return self.redirect(self.request.current_route_url())
+
+ context = self.preferences_get_context(user, current_user)
+ return self.render_to_response('preferences', context)
+
+ def my_preferences(self):
+ """
+ View to modify preferences for the current user.
+ """
+ user = self.request.user
+ if not user:
+ raise self.forbidden()
+ return self.preferences(user=user)
+
+ def preferences_get_context(self, user, current_user):
+ simple_settings = self.preferences_get_simple_settings(user)
+ context = self.configure_get_context(simple_settings=simple_settings,
+ input_file_templates=False)
+
+ instance_title = self.get_instance_title(user)
+ context.update({
+ 'user': user,
+ 'instance': user,
+ 'instance_title': instance_title,
+ 'instance_url': self.get_action_url('view', user),
+ 'config_title': instance_title,
+ 'config_preferences': True,
+ 'current_user': current_user,
+ })
+
+ if current_user:
+ context.update({
+ 'index_url': None,
+ 'index_title': instance_title,
+ })
+
+ # theme style options
+ options = [{'value': None, 'label': "default"}]
+ styles = self.rattail_config.getlist('tailbone', 'themes.styles',
+ default=[])
+ for name in styles:
+ css = self.rattail_config.get('tailbone',
+ 'themes.style.{}'.format(name))
+ if css:
+ options.append({'value': css, 'label': name})
+ context['buefy_css_options'] = options
+
+ return context
+
+ def preferences_get_simple_settings(self, user):
+ """
+ This method is conceptually the same as for
+ :meth:`~tailbone.views.master.MasterView.configure_get_simple_settings()`.
+ See its docs for more info.
+
+ The only difference here is that we are given a user account,
+ so the settings involved should only pertain to that user.
+ """
+ return [
+
+ # display
+ {'section': 'tailbone.{}'.format(user.uuid),
+ 'option': 'buefy_css'},
+ ]
+
+ def preferences_gather_settings(self, data, user):
+ simple_settings = self.preferences_get_simple_settings(user)
+ return self.configure_gather_settings(
+ data, simple_settings=simple_settings, input_file_templates=False)
+
+ def preferences_remove_settings(self, user):
+ simple_settings = self.preferences_get_simple_settings(user)
+ self.configure_remove_settings(simple_settings=simple_settings,
+ input_file_templates=False)
+
@classmethod
def defaults(cls, config):
cls._user_defaults(config)
@@ -459,7 +565,9 @@ class UserView(PrincipalMasterView):
"""
Provide extra default configuration for the User master view.
"""
+ route_prefix = cls.get_route_prefix()
permission_prefix = cls.get_permission_prefix()
+ instance_url_prefix = cls.get_instance_url_prefix()
model_title = cls.get_model_title()
# view/edit roles
@@ -468,6 +576,23 @@ class UserView(PrincipalMasterView):
config.add_tailbone_permission(permission_prefix, '{}.edit_roles'.format(permission_prefix),
"Edit the Roles to which a {} belongs".format(model_title))
+ # edit preferences for any user
+ config.add_tailbone_permission(permission_prefix,
+ '{}.preferences'.format(permission_prefix),
+ "Edit preferences for any {}".format(model_title))
+ config.add_route('{}.preferences'.format(route_prefix),
+ '{}/preferences'.format(instance_url_prefix))
+ config.add_view(cls, attr='preferences',
+ route_name='{}.preferences'.format(route_prefix),
+ permission='{}.preferences'.format(permission_prefix))
+
+ # edit "my" preferences (for current user)
+ config.add_route('my.preferences',
+ '/my/preferences')
+ config.add_view(cls, attr='my_preferences',
+ route_name='my.preferences')
+
+
# TODO: deprecate / remove this
UsersView = UserView
@@ -476,7 +601,7 @@ class UserEventView(MasterView):
"""
Master view for all user events
"""
- model_class = model.UserEvent
+ model_class = UserEvent
url_prefix = '/user-events'
viewable = False
creatable = False
@@ -492,10 +617,12 @@ class UserEventView(MasterView):
def get_data(self, session=None):
query = super(UserEventView, self).get_data(session=session)
+ model = self.model
return query.join(model.User)
def configure_grid(self, g):
super(UserEventView, self).configure_grid(g)
+ model = self.model
g.set_joiner('person', lambda q: q.outerjoin(model.Person))
g.set_sorter('user', model.User.username)
g.set_sorter('person', model.Person.display_name)
@@ -522,8 +649,12 @@ UserEventsView = UserEventView
def defaults(config, **kwargs):
base = globals()
- kwargs.get('UserView', base['UserView']).defaults(config)
- kwargs.get('UserEventView', base['UserEventView']).defaults(config)
+
+ UserView = kwargs.get('UserView', base['UserView'])
+ UserView.defaults(config)
+
+ UserEventView = kwargs.get('UserEventView', base['UserEventView'])
+ UserEventView.defaults(config)
def includeme(config):