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> </b-button>
</%def> </%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()"> <%def name="buttons_row()">
<div class="level"> <div class="level">
<div class="level-left"> <div class="level-left">
<div class="level-item"> <div class="level-item">
<p class="block"> ${self.intro_message()}
This page lets you modify the configuration for ${config_title}.
</p>
</div> </div>
<div class="level-item"> <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')} ${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
% endif % endif
${h.link_to("Change Password", url('change_password'), class_='navbar-item')} ${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')} ${h.link_to("Logout", url('logout'), class_='navbar-item')}
</div> </div>
</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 % endif
</%def> </%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()} ${parent.body()}

View file

@ -4290,6 +4290,7 @@ class MasterView(View):
'type': bool, 'type': bool,
'value': config.getbool('rattail.batch', 'value': config.getbool('rattail.batch',
'purchase.allow_cases'), 'purchase.allow_cases'),
'save_if_empty': False,
} }
Note that some of the above is optional, in particular it Note that some of the above is optional, in particular it
@ -4316,8 +4317,10 @@ class MasterView(View):
return '{}.{}'.format(simple['section'], return '{}.{}'.format(simple['section'],
simple['option']) simple['option'])
def configure_get_context(self): def configure_get_context(self, simple_settings=None,
input_file_templates=True):
context = {} context = {}
if simple_settings is None:
simple_settings = self.configure_get_simple_settings() simple_settings = self.configure_get_simple_settings()
if simple_settings: if simple_settings:
@ -4342,7 +4345,7 @@ class MasterView(View):
context['simple_settings'] = settings context['simple_settings'] = settings
# add settings for downloadable input file templates, if any # 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 = {} settings = {}
file_options = {} file_options = {}
file_option_dirs = {} file_option_dirs = {}
@ -4359,10 +4362,12 @@ class MasterView(View):
return context return context
def configure_gather_settings(self, data): def configure_gather_settings(self, data, simple_settings=None,
input_file_templates=True):
settings = [] settings = []
# maybe collect "simple" settings # maybe collect "simple" settings
if simple_settings is None:
simple_settings = self.configure_get_simple_settings() simple_settings = self.configure_get_simple_settings()
if simple_settings: if simple_settings:
@ -4377,11 +4382,14 @@ class MasterView(View):
else: else:
value = six.text_type(value) 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, settings.append({'name': name,
'value': value}) 'value': value})
# maybe also collect input file template settings # 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(): for template in self.normalize_input_file_templates():
# mode # mode
@ -4401,16 +4409,18 @@ class MasterView(View):
return settings return settings
def configure_remove_settings(self): def configure_remove_settings(self, simple_settings=None,
input_file_templates=True):
model = self.model model = self.model
names = [] names = []
if simple_settings is None:
simple_settings = self.configure_get_simple_settings() simple_settings = self.configure_get_simple_settings()
if simple_settings: if simple_settings:
names.extend([self.configure_get_name_for_simple_setting(simple) names.extend([self.configure_get_name_for_simple_setting(simple)
for simple in simple_settings]) 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(): for template in self.normalize_input_file_templates():
names.extend([ names.extend([
template['setting_mode'], template['setting_mode'],

View file

@ -26,12 +26,10 @@ User Views
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import copy
import six import six
from sqlalchemy import orm 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, from rattail.db.auth import (administrator_role, guest_role,
authenticated_role, set_user_password) authenticated_role, set_user_password)
@ -40,8 +38,7 @@ from deform import widget as dfwidget
from webhelpers2.html import HTML, tags from webhelpers2.html import HTML, tags
from tailbone import forms from tailbone import forms
from tailbone.db import Session from tailbone.views import MasterView, View
from tailbone.views import MasterView
from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer
@ -49,9 +46,9 @@ class UserView(PrincipalMasterView):
""" """
Master view for the User model. Master view for the User model.
""" """
model_class = model.User model_class = User
has_rows = True has_rows = True
model_row_class = model.UserEvent model_row_class = UserEvent
has_versions = True has_versions = True
touchable = True touchable = True
@ -99,6 +96,7 @@ class UserView(PrincipalMasterView):
def query(self, session): def query(self, session):
query = super(UserView, self).query(session) query = super(UserView, self).query(session)
model = self.model
# bring in the related Person(s) # bring in the related Person(s)
query = query.outerjoin(model.Person)\ query = query.outerjoin(model.Person)\
@ -108,6 +106,7 @@ class UserView(PrincipalMasterView):
def configure_grid(self, g): def configure_grid(self, g):
super(UserView, self).configure_grid(g) super(UserView, self).configure_grid(g)
model = self.model
del g.filters['salt'] del g.filters['salt']
g.filters['username'].default_active = True g.filters['username'].default_active = True
@ -160,6 +159,7 @@ class UserView(PrincipalMasterView):
return not self.user_is_protected(user) return not self.user_is_protected(user)
def unique_username(self, node, value): def unique_username(self, node, value):
model = self.model
query = self.Session.query(model.User)\ query = self.Session.query(model.User)\
.filter(model.User.username == value) .filter(model.User.username == value)
if self.editing: if self.editing:
@ -181,6 +181,7 @@ class UserView(PrincipalMasterView):
def configure_form(self, f): def configure_form(self, f):
super(UserView, self).configure_form(f) super(UserView, self).configure_form(f)
model = self.model
user = f.model_instance user = f.model_instance
# username # username
@ -265,6 +266,7 @@ class UserView(PrincipalMasterView):
f.remove('set_password') f.remove('set_password')
def get_possible_roles(self): def get_possible_roles(self):
model = self.model
# some roles should never have users "belong" to them # some roles should never have users "belong" to them
excluded = [ excluded = [
@ -281,6 +283,7 @@ class UserView(PrincipalMasterView):
.order_by(model.Role.name) .order_by(model.Role.name)
def objectify(self, form, data=None): def objectify(self, form, data=None):
model = self.model
# create/update user as per normal # create/update user as per normal
if data is None: if data is None:
@ -328,6 +331,7 @@ class UserView(PrincipalMasterView):
if 'roles' not in data: if 'roles' not in data:
return return
model = self.model
old_roles = set([r.uuid for r in user.roles]) old_roles = set([r.uuid for r in user.roles])
new_roles = data['roles'] new_roles = data['roles']
admin = administrator_role(self.Session()) admin = administrator_role(self.Session())
@ -389,6 +393,7 @@ class UserView(PrincipalMasterView):
return HTML.tag('ul', c=items) return HTML.tag('ul', c=items)
def get_row_data(self, user): def get_row_data(self, user):
model = self.model
return self.Session.query(model.UserEvent)\ return self.Session.query(model.UserEvent)\
.filter(model.UserEvent.user == user) .filter(model.UserEvent.user == user)
@ -402,6 +407,7 @@ class UserView(PrincipalMasterView):
g.main_actions = [] g.main_actions = []
def get_version_child_classes(self): def get_version_child_classes(self):
model = self.model
return [ return [
(model.UserRole, 'user_uuid'), (model.UserRole, 'user_uuid'),
] ]
@ -409,6 +415,7 @@ class UserView(PrincipalMasterView):
def find_principals_with_permission(self, session, permission): def find_principals_with_permission(self, session, permission):
app = self.get_rattail_app() app = self.get_rattail_app()
auth = app.get_auth_handler() auth = app.get_auth_handler()
model = self.model
# TODO: this should search Permission table instead, and work backward to User? # TODO: this should search Permission table instead, and work backward to User?
all_users = session.query(model.User)\ all_users = session.query(model.User)\
@ -448,6 +455,105 @@ class UserView(PrincipalMasterView):
assert not removing._roles assert not removing._roles
self.Session.delete(removing) 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 @classmethod
def defaults(cls, config): def defaults(cls, config):
cls._user_defaults(config) cls._user_defaults(config)
@ -459,7 +565,9 @@ class UserView(PrincipalMasterView):
""" """
Provide extra default configuration for the User master view. Provide extra default configuration for the User master view.
""" """
route_prefix = cls.get_route_prefix()
permission_prefix = cls.get_permission_prefix() permission_prefix = cls.get_permission_prefix()
instance_url_prefix = cls.get_instance_url_prefix()
model_title = cls.get_model_title() model_title = cls.get_model_title()
# view/edit roles # view/edit roles
@ -468,6 +576,23 @@ class UserView(PrincipalMasterView):
config.add_tailbone_permission(permission_prefix, '{}.edit_roles'.format(permission_prefix), config.add_tailbone_permission(permission_prefix, '{}.edit_roles'.format(permission_prefix),
"Edit the Roles to which a {} belongs".format(model_title)) "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 # TODO: deprecate / remove this
UsersView = UserView UsersView = UserView
@ -476,7 +601,7 @@ class UserEventView(MasterView):
""" """
Master view for all user events Master view for all user events
""" """
model_class = model.UserEvent model_class = UserEvent
url_prefix = '/user-events' url_prefix = '/user-events'
viewable = False viewable = False
creatable = False creatable = False
@ -492,10 +617,12 @@ class UserEventView(MasterView):
def get_data(self, session=None): def get_data(self, session=None):
query = super(UserEventView, self).get_data(session=session) query = super(UserEventView, self).get_data(session=session)
model = self.model
return query.join(model.User) return query.join(model.User)
def configure_grid(self, g): def configure_grid(self, g):
super(UserEventView, self).configure_grid(g) super(UserEventView, self).configure_grid(g)
model = self.model
g.set_joiner('person', lambda q: q.outerjoin(model.Person)) g.set_joiner('person', lambda q: q.outerjoin(model.Person))
g.set_sorter('user', model.User.username) g.set_sorter('user', model.User.username)
g.set_sorter('person', model.Person.display_name) g.set_sorter('person', model.Person.display_name)
@ -522,8 +649,12 @@ UserEventsView = UserEventView
def defaults(config, **kwargs): def defaults(config, **kwargs):
base = globals() 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): def includeme(config):