Add initial support for editing user preferences

by default this exposes just one setting which has only one possible
value, so not very useful.  but can override as needed
This commit is contained in:
Lance Edgar 2022-02-14 19:19:33 -06:00
parent 6093be43c9
commit 962d31c4c2
6 changed files with 239 additions and 24 deletions

View file

@ -30,14 +30,24 @@
</b-button>
</%def>
<%def name="intro_message()">
<p class="block">
This page lets you modify the
% if config_preferences is not Undefined and config_preferences:
preferences
% else:
configuration
% endif
for ${config_title}.
</p>
</%def>
<%def name="buttons_row()">
<div class="level">
<div class="level-left">
<div class="level-item">
<p class="block">
This page lets you modify the configuration for ${config_title}.
</p>
${self.intro_message()}
</div>
<div class="level-item">

View file

@ -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')}
</div>
</div>

View file

@ -0,0 +1,55 @@
## -*- coding: utf-8; -*-
<%inherit file="/configure.mako" />
<%def name="title()">
% if current_user:
Edit Preferences
% else:
${index_title} &raquo; ${instance_title} &raquo; Preferences
% endif
</%def>
<%def name="content_title()">Preferences</%def>
<%def name="intro_message()">
<p class="block">
% if current_user:
This page lets you modify your preferences.
% else:
This page lets you modify the preferences for ${config_title}.
% endif
</p>
</%def>
<%def name="form_content()">
<h3 class="block is-size-3">Display</h3>
<div class="block" style="padding-left: 2rem;">
<b-field label="Theme Style">
<b-select name="tailbone.${user.uuid}.buefy_css"
v-model="simpleSettings['tailbone.${user.uuid}.buefy_css']"
@input="settingsNeedSaved = true">
<option v-for="option in buefyCSSOptions"
:key="option.value"
:value="option.value">
{{ option.label }}
</option>
</b-select>
</b-field>
</div>
</%def>
<%def name="modify_this_page_vars()">
${parent.modify_this_page_vars()}
<script type="text/javascript">
ThisPageData.buefyCSSOptions = ${json.dumps(buefy_css_options)|n}
</script>
</%def>
${parent.body()}

View file

@ -14,4 +14,12 @@
% endif
</%def>
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if master.has_perm('preferences'):
<li>${h.link_to("Edit User Preferences", action_url('preferences', instance))}</li>
% endif
</%def>
${parent.body()}

View file

@ -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,8 +4317,10 @@ 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 = {}
if simple_settings is None:
simple_settings = self.configure_get_simple_settings()
if simple_settings:
@ -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,10 +4362,12 @@ 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
if simple_settings is None:
simple_settings = self.configure_get_simple_settings()
if simple_settings:
@ -4377,11 +4382,14 @@ class MasterView(View):
else:
value = six.text_type(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 = []
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'],

View file

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