From 962d31c4c2d7fc6b2c7c5fd2a0a4ee9e747b7c2b Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Mon, 14 Feb 2022 19:19:33 -0600 Subject: [PATCH] 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 --- tailbone/templates/configure.mako | 16 ++- tailbone/templates/themes/falafel/base.mako | 1 + tailbone/templates/users/preferences.mako | 55 +++++++ tailbone/templates/users/view.mako | 8 ++ tailbone/views/master.py | 32 +++-- tailbone/views/users.py | 151 ++++++++++++++++++-- 6 files changed, 239 insertions(+), 24 deletions(-) create mode 100644 tailbone/templates/users/preferences.mako 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 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 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 name="content_title()">Preferences + +<%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 name="form_content()"> + +

Display

+
+ + + + + + + + +
+ + +<%def name="modify_this_page_vars()"> + ${parent.modify_this_page_vars()} + + + + +${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 name="context_menu_items()"> + ${parent.context_menu_items()} + % if master.has_perm('preferences'): +
  • ${h.link_to("Edit User Preferences", action_url('preferences', instance))}
  • + % endif + + + ${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):