c002d3d182
also various other changes in pursuit of that. so far tokens are only accepted by web API and not traditional web app
745 lines
27 KiB
Python
745 lines
27 KiB
Python
# -*- coding: utf-8; -*-
|
|
################################################################################
|
|
#
|
|
# Rattail -- Retail Software Framework
|
|
# Copyright © 2010-2023 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/>.
|
|
#
|
|
################################################################################
|
|
"""
|
|
User Views
|
|
"""
|
|
|
|
import sqlalchemy as sa
|
|
from sqlalchemy import orm
|
|
|
|
from rattail.db.model import User, UserEvent
|
|
from rattail.db.auth import (administrator_role, guest_role,
|
|
authenticated_role, set_user_password)
|
|
|
|
import colander
|
|
from deform import widget as dfwidget
|
|
from webhelpers2.html import HTML, tags
|
|
|
|
from tailbone import forms
|
|
from tailbone.views import MasterView, View
|
|
from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer
|
|
from tailbone.util import raw_datetime
|
|
|
|
|
|
class UserView(PrincipalMasterView):
|
|
"""
|
|
Master view for the User model.
|
|
"""
|
|
model_class = User
|
|
has_rows = True
|
|
model_row_class = UserEvent
|
|
has_versions = True
|
|
touchable = True
|
|
mergeable = True
|
|
|
|
labels = {
|
|
'api_tokens': "API Tokens",
|
|
}
|
|
|
|
grid_columns = [
|
|
'username',
|
|
'person',
|
|
'active',
|
|
'local_only',
|
|
]
|
|
|
|
form_fields = [
|
|
'username',
|
|
'person',
|
|
'first_name_',
|
|
'last_name_',
|
|
'display_name_',
|
|
'active',
|
|
'active_sticky',
|
|
'set_password',
|
|
'prevent_password_change',
|
|
'api_tokens',
|
|
'roles',
|
|
'permissions',
|
|
]
|
|
|
|
row_grid_columns = [
|
|
'type_code',
|
|
'occurred',
|
|
]
|
|
|
|
def __init__(self, request):
|
|
super(UserView, self).__init__(request)
|
|
app = self.get_rattail_app()
|
|
|
|
# always get a reference to the auth/merge handler
|
|
self.auth_handler = app.get_auth_handler()
|
|
self.merge_handler = self.auth_handler
|
|
|
|
def query(self, session):
|
|
query = super(UserView, self).query(session)
|
|
model = self.model
|
|
|
|
# bring in the related Person(s)
|
|
query = query.outerjoin(model.Person)\
|
|
.options(orm.joinedload(model.User.person))
|
|
|
|
return query
|
|
|
|
def configure_grid(self, g):
|
|
super(UserView, self).configure_grid(g)
|
|
model = self.model
|
|
|
|
del g.filters['salt']
|
|
g.filters['username'].default_active = True
|
|
g.filters['username'].default_verb = 'contains'
|
|
g.filters['active'].default_active = True
|
|
g.filters['active'].default_verb = 'is_true'
|
|
g.filters['person'] = g.make_filter('person', model.Person.display_name,
|
|
default_active=True, default_verb='contains')
|
|
|
|
# password
|
|
g.set_filter('password', model.User.password,
|
|
verbs=['is_null', 'is_not_null'])
|
|
|
|
g.set_sorter('person', model.Person.display_name)
|
|
g.set_sorter('first_name', model.Person.first_name)
|
|
g.set_sorter('last_name', model.Person.last_name)
|
|
g.set_sorter('display_name', model.Person.display_name)
|
|
g.set_sort_defaults('username')
|
|
|
|
g.set_label('person', "Person's Name")
|
|
|
|
g.set_link('username')
|
|
g.set_link('person')
|
|
g.set_link('first_name')
|
|
g.set_link('last_name')
|
|
g.set_link('display_name')
|
|
|
|
def grid_extra_class(self, user, i):
|
|
if not user.active:
|
|
return 'warning'
|
|
|
|
def editable_instance(self, user):
|
|
"""
|
|
If the given user is "protected" then we only allow edit if current
|
|
user is "root". But if the given user is not protected, this simply
|
|
returns ``True``.
|
|
"""
|
|
if self.request.is_root:
|
|
return True
|
|
return not self.user_is_protected(user)
|
|
|
|
def deletable_instance(self, user):
|
|
"""
|
|
If the given user is "protected" then we only allow delete if current
|
|
user is "root". But if the given user is not protected, this simply
|
|
returns ``True``.
|
|
"""
|
|
if self.request.is_root:
|
|
return True
|
|
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:
|
|
user = self.get_instance()
|
|
query = query.filter(model.User.uuid != user.uuid)
|
|
if query.count():
|
|
raise colander.Invalid(node, "Username must be unique")
|
|
|
|
def valid_person(self, node, value):
|
|
"""
|
|
Make sure ``value`` corresponds to an existing
|
|
``Person.uuid``.
|
|
"""
|
|
if value:
|
|
model = self.model
|
|
person = self.Session.get(model.Person, value)
|
|
if not person:
|
|
raise colander.Invalid(node, "Person not found (you must *select* a record)")
|
|
|
|
def configure_form(self, f):
|
|
super(UserView, self).configure_form(f)
|
|
model = self.model
|
|
user = f.model_instance
|
|
|
|
# username
|
|
f.set_validator('username', self.unique_username)
|
|
|
|
# person
|
|
f.set_renderer('person', self.render_person)
|
|
if self.creating or self.editing:
|
|
if 'person' in f.fields:
|
|
f.replace('person', 'person_uuid')
|
|
f.set_node('person_uuid', colander.String(), missing=colander.null)
|
|
person_display = ""
|
|
if self.request.method == 'POST':
|
|
if self.request.POST.get('person_uuid'):
|
|
person = self.Session.get(model.Person, self.request.POST['person_uuid'])
|
|
if person:
|
|
person_display = str(person)
|
|
elif self.editing:
|
|
person_display = str(user.person or '')
|
|
people_url = self.request.route_url('people.autocomplete')
|
|
f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget(
|
|
field_display=person_display, service_url=people_url))
|
|
f.set_validator('person_uuid', self.valid_person)
|
|
f.set_label('person_uuid', "Person")
|
|
|
|
# person name(s)
|
|
if self.editing:
|
|
# must explicitly set default, for "custom" field names
|
|
f.set_default('first_name_', user.first_name or "")
|
|
f.set_default('last_name_', user.last_name or "")
|
|
f.set_default('display_name_', user.display_name or "")
|
|
elif not self.creating:
|
|
# must provide custom renderer as well
|
|
f.set_renderer('first_name_', self.render_person_name)
|
|
f.set_renderer('last_name_', self.render_person_name)
|
|
f.set_renderer('display_name_', self.render_person_name)
|
|
|
|
# set_password
|
|
if self.editing and user.prevent_password_change and not self.request.is_root:
|
|
f.remove('set_password')
|
|
else:
|
|
f.set_widget('set_password', dfwidget.CheckedPasswordWidget())
|
|
# if self.creating:
|
|
# f.set_required('password')
|
|
|
|
# api_tokens
|
|
if self.creating or self.editing:
|
|
f.remove('api_tokens')
|
|
elif self.has_perm('manage_api_tokens'):
|
|
f.set_renderer('api_tokens', self.render_api_tokens)
|
|
f.set_vuejs_component_kwargs(**{':apiTokens': 'apiTokens',
|
|
'@api-new-token': 'apiNewToken',
|
|
'@api-token-delete': 'apiTokenDelete'})
|
|
else:
|
|
f.remove('api_tokens')
|
|
|
|
# roles
|
|
f.set_renderer('roles', self.render_roles)
|
|
if self.creating or self.editing:
|
|
if not self.has_perm('edit_roles'):
|
|
f.remove_field('roles')
|
|
else:
|
|
roles = self.get_possible_roles().all()
|
|
role_values = [(s.uuid, str(s)) for s in roles]
|
|
f.set_node('roles', colander.Set())
|
|
size = len(roles)
|
|
if size < 3:
|
|
size = 3
|
|
elif size > 20:
|
|
size = 20
|
|
f.set_widget('roles', dfwidget.SelectWidget(multiple=True,
|
|
size=size,
|
|
values=role_values))
|
|
if self.editing:
|
|
f.set_default('roles', [r.uuid for r in user.roles])
|
|
elif not self.has_perm('view_roles'):
|
|
f.remove_field('roles')
|
|
|
|
f.set_label('display_name', "Full Name")
|
|
|
|
# # hm this should work according to MDN but doesn't seem to...
|
|
# # https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
|
# fs.username.attrs(autocomplete='new-password')
|
|
# fs.password.attrs(autocomplete='new-password')
|
|
# fs.confirm_password.attrs(autocomplete='new-password')
|
|
|
|
if self.viewing:
|
|
permissions = self.request.registry.settings.get('tailbone_permissions', {})
|
|
f.set_renderer('permissions', PermissionsRenderer(request=self.request,
|
|
permissions=permissions,
|
|
include_guest=True,
|
|
include_authenticated=True))
|
|
else:
|
|
f.remove('permissions')
|
|
|
|
if self.viewing or self.deleting:
|
|
f.remove('set_password')
|
|
|
|
def render_api_tokens(self, user, field):
|
|
route_prefix = self.get_route_prefix()
|
|
permission_prefix = self.get_permission_prefix()
|
|
|
|
factory = self.get_grid_factory()
|
|
g = factory(
|
|
key='{}.api_tokens'.format(route_prefix),
|
|
data=[],
|
|
columns=['description', 'created'],
|
|
main_actions=[
|
|
self.make_action('delete', icon='trash',
|
|
click_handler="$emit('api-token-delete', props.row)")])
|
|
|
|
button = self.make_buefy_button("New", is_primary=True,
|
|
icon_left='plus',
|
|
**{'@click': "$emit('api-new-token')"})
|
|
|
|
table = HTML.literal(
|
|
g.render_buefy_table_element(data_prop='apiTokens'))
|
|
|
|
return HTML.tag('div', c=[button, table])
|
|
|
|
def add_api_token(self):
|
|
user = self.get_instance()
|
|
data = self.request.json_body
|
|
|
|
token = self.auth_handler.add_api_token(user, data['description'])
|
|
self.Session.flush()
|
|
|
|
return {'ok': True,
|
|
'raw_token': token.token_string,
|
|
'tokens': self.get_api_tokens(user)}
|
|
|
|
def delete_api_token(self):
|
|
model = self.model
|
|
user = self.get_instance()
|
|
data = self.request.json_body
|
|
|
|
token = self.Session.get(model.UserAPIToken, data['uuid'])
|
|
if not token:
|
|
return {'error': "API token not found"}
|
|
|
|
if token.user is not user:
|
|
return {'error': "API token not found"}
|
|
|
|
self.auth_handler.delete_api_token(token)
|
|
self.Session.flush()
|
|
|
|
return {'ok': True,
|
|
'tokens': self.get_api_tokens(user)}
|
|
|
|
def template_kwargs_view(self, **kwargs):
|
|
kwargs = super(UserView, self).template_kwargs_view(**kwargs)
|
|
user = kwargs['instance']
|
|
|
|
kwargs['api_tokens_data'] = self.get_api_tokens(user)
|
|
|
|
return kwargs
|
|
|
|
def get_api_tokens(self, user):
|
|
tokens = []
|
|
for token in reversed(user.api_tokens):
|
|
tokens.append({
|
|
'uuid': token.uuid,
|
|
'description': token.description,
|
|
'created': raw_datetime(self.rattail_config, token.created),
|
|
})
|
|
return tokens
|
|
|
|
def get_possible_roles(self):
|
|
model = self.model
|
|
|
|
# some roles should never have users "belong" to them
|
|
excluded = [
|
|
guest_role(self.Session()).uuid,
|
|
authenticated_role(self.Session()).uuid,
|
|
]
|
|
|
|
# only allow "root" user to change true admin role membership
|
|
if not self.request.is_root:
|
|
excluded.append(administrator_role(self.Session()).uuid)
|
|
|
|
# basic list, minus exclusions so far
|
|
roles = self.Session.query(model.Role)\
|
|
.filter(~model.Role.uuid.in_(excluded))
|
|
|
|
# only allow "admin" user to change admin-ish role memberships
|
|
if not self.request.is_admin:
|
|
roles = roles.filter(sa.or_(
|
|
model.Role.adminish == False,
|
|
model.Role.adminish == None))
|
|
|
|
return roles.order_by(model.Role.name)
|
|
|
|
def objectify(self, form, data=None):
|
|
model = self.model
|
|
|
|
# create/update user as per normal
|
|
if data is None:
|
|
data = form.validated
|
|
user = super(UserView, self).objectify(form, data)
|
|
|
|
# create/update person as needed
|
|
names = {}
|
|
if 'first_name_' in form and data['first_name_']:
|
|
names['first'] = data['first_name_']
|
|
if 'last_name_' in form and data['last_name_']:
|
|
names['last'] = data['last_name_']
|
|
if 'display_name_' in form and data['display_name_']:
|
|
names['full'] = data['display_name_']
|
|
# we will not have a person reference yet, when creating new user. if
|
|
# that is the case, go ahead and load it, if specified.
|
|
if self.creating and user.person_uuid:
|
|
self.Session.add(user)
|
|
self.Session.flush()
|
|
# note, do *not* create new person unless name(s) provided
|
|
if not user.person and any([n for n in names.values()]):
|
|
user.person = model.Person()
|
|
if user.person:
|
|
app = self.get_rattail_app()
|
|
handler = app.get_people_handler()
|
|
handler.update_names(user.person, **names)
|
|
|
|
# force "local only" flag unless global access granted
|
|
if self.secure_global_objects:
|
|
if not self.has_perm('view_global'):
|
|
user.person.local_only = True
|
|
|
|
# maybe set user password
|
|
if 'set_password' in form and data['set_password']:
|
|
set_user_password(user, data['set_password'])
|
|
|
|
# update roles for user
|
|
self.update_roles(user, data)
|
|
|
|
return user
|
|
|
|
def update_roles(self, user, data):
|
|
if not self.has_perm('edit_roles'):
|
|
return
|
|
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())
|
|
|
|
# add any new roles for the user, taking care not to add the admin role
|
|
# unless acting as root
|
|
for uuid in new_roles:
|
|
if uuid not in old_roles:
|
|
if self.request.is_root or uuid != admin.uuid:
|
|
user._roles.append(model.UserRole(role_uuid=uuid))
|
|
|
|
# also record a change to the role, for datasync.
|
|
# this is done "just in case" the role is to be
|
|
# synced to all nodes
|
|
if self.Session().rattail_record_changes:
|
|
self.Session.add(model.Change(class_name='Role',
|
|
instance_uuid=uuid,
|
|
deleted=False))
|
|
|
|
# remove any roles which were *not* specified, although must take care
|
|
# not to remove admin role, unless acting as root
|
|
for uuid in old_roles:
|
|
if uuid not in new_roles:
|
|
if self.request.is_root or uuid != admin.uuid:
|
|
role = self.Session.get(model.Role, uuid)
|
|
user.roles.remove(role)
|
|
|
|
# also record a change to the role, for datasync.
|
|
# this is done "just in case" the role is to be
|
|
# synced to all nodes
|
|
if self.Session().rattail_record_changes:
|
|
self.Session.add(model.Change(class_name='Role',
|
|
instance_uuid=uuid,
|
|
deleted=False))
|
|
|
|
def render_person(self, user, field):
|
|
person = user.person
|
|
if not person:
|
|
return ""
|
|
text = str(person)
|
|
url = self.request.route_url('people.view', uuid=person.uuid)
|
|
return tags.link_to(person, url)
|
|
|
|
def render_person_name(self, user, field):
|
|
if not field.endswith('_'):
|
|
return ""
|
|
name = getattr(user, field[:-1], None)
|
|
if not name:
|
|
return ""
|
|
return str(name)
|
|
|
|
def render_roles(self, user, field):
|
|
roles = sorted(user.roles, key=lambda r: r.name)
|
|
items = []
|
|
for role in roles:
|
|
text = role.name
|
|
url = self.request.route_url('roles.view', uuid=role.uuid)
|
|
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
|
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)
|
|
|
|
def configure_row_grid(self, g):
|
|
super(UserView, self).configure_row_grid(g)
|
|
g.width = 'half'
|
|
g.filterable = False
|
|
g.set_sort_defaults('occurred', 'desc')
|
|
g.set_enum('type_code', self.enum.USER_EVENT)
|
|
g.set_label('type_code', "Event Type")
|
|
g.main_actions = []
|
|
|
|
def get_version_child_classes(self):
|
|
model = self.model
|
|
return [
|
|
(model.UserRole, 'user_uuid'),
|
|
]
|
|
|
|
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)\
|
|
.filter(model.User.active == True)\
|
|
.order_by(model.User.username)\
|
|
.options(orm.joinedload(model.User._roles)\
|
|
.joinedload(model.UserRole.role)\
|
|
.joinedload(model.Role._permissions))
|
|
users = []
|
|
for user in all_users:
|
|
if auth.has_permission(session, user, permission):
|
|
users.append(user)
|
|
return users
|
|
|
|
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)
|
|
cls._principal_defaults(config)
|
|
cls._defaults(config)
|
|
|
|
@classmethod
|
|
def _user_defaults(cls, config):
|
|
"""
|
|
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
|
|
config.add_tailbone_permission(permission_prefix, '{}.view_roles'.format(permission_prefix),
|
|
"View the Roles to which a {} belongs".format(model_title))
|
|
config.add_tailbone_permission(permission_prefix, '{}.edit_roles'.format(permission_prefix),
|
|
"Edit the Roles to which a {} belongs".format(model_title))
|
|
|
|
# manage API tokens
|
|
config.add_tailbone_permission(permission_prefix,
|
|
'{}.manage_api_tokens'.format(permission_prefix),
|
|
"Manage API tokens for any {}".format(model_title))
|
|
config.add_route('{}.add_api_token'.format(route_prefix),
|
|
'{}/add-api-token'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='add_api_token',
|
|
route_name='{}.add_api_token'.format(route_prefix),
|
|
permission='{}.manage_api_tokens'.format(permission_prefix),
|
|
renderer='json')
|
|
config.add_route('{}.delete_api_token'.format(route_prefix),
|
|
'{}/delete-api-token'.format(instance_url_prefix),
|
|
request_method='POST')
|
|
config.add_view(cls, attr='delete_api_token',
|
|
route_name='{}.delete_api_token'.format(route_prefix),
|
|
permission='{}.manage_api_tokens'.format(permission_prefix),
|
|
renderer='json')
|
|
|
|
# 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
|
|
|
|
|
|
class UserEventView(MasterView):
|
|
"""
|
|
Master view for all user events
|
|
"""
|
|
model_class = UserEvent
|
|
url_prefix = '/user-events'
|
|
viewable = False
|
|
creatable = False
|
|
editable = False
|
|
deletable = False
|
|
|
|
grid_columns = [
|
|
'user',
|
|
'person',
|
|
'type_code',
|
|
'occurred',
|
|
]
|
|
|
|
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)
|
|
g.filters['user'] = g.make_filter('user', model.User.username)
|
|
g.filters['person'] = g.make_filter('person', model.Person.display_name)
|
|
g.set_enum('type_code', self.enum.USER_EVENT)
|
|
g.set_type('occurred', 'datetime')
|
|
g.set_renderer('user', self.render_user)
|
|
g.set_renderer('person', self.render_person)
|
|
g.set_sort_defaults('occurred', 'desc')
|
|
g.set_label('user', "Username")
|
|
g.set_label('type_code', "Event Type")
|
|
|
|
def render_user(self, event, column):
|
|
return event.user.username
|
|
|
|
def render_person(self, event, column):
|
|
if event.user.person:
|
|
return event.user.person.display_name
|
|
|
|
# TODO: deprecate / remove this
|
|
UserEventsView = UserEventView
|
|
|
|
|
|
def defaults(config, **kwargs):
|
|
base = globals()
|
|
|
|
UserView = kwargs.get('UserView', base['UserView'])
|
|
UserView.defaults(config)
|
|
|
|
UserEventView = kwargs.get('UserEventView', base['UserEventView'])
|
|
UserEventView.defaults(config)
|
|
|
|
|
|
def includeme(config):
|
|
defaults(config)
|