Convert User pages to use master view.

And of course make some more tweaks to new grids etc.
This commit is contained in:
Lance Edgar 2015-08-11 23:19:41 -05:00
parent 9cfbc918e7
commit af07f477dc
11 changed files with 269 additions and 289 deletions

View file

@ -45,6 +45,10 @@ class MasterView(View):
"""
Base "master" view class. All model master views should derive from this.
"""
creating = False
viewing = False
editing = False
deleting = False
##############################
# Available Views
@ -69,7 +73,8 @@ class MasterView(View):
"""
View for creating a new model record.
"""
form = self.make_form(self.model_class, creating=True)
self.creating = True
form = self.make_form(self.model_class)
if self.request.method == 'POST':
if form.validate():
form.save()
@ -83,11 +88,9 @@ class MasterView(View):
"""
View for viewing details of an existing model record.
"""
key = self.request.matchdict[self.get_model_key()]
instance = Session.query(self.model_class).get(key)
if not instance:
return HTTPNotFound()
form = self.make_form(instance, readonly=True)
self.viewing = True
instance = self.get_instance()
form = self.make_form(instance)
return self.render_to_response('view', {
'instance': instance, 'form': form})
@ -95,11 +98,9 @@ class MasterView(View):
"""
View for editing an existing model record.
"""
key = self.request.matchdict[self.get_model_key()]
instance = Session.query(self.model_class).get(key)
if not instance:
return HTTPNotFound()
form = self.make_form(instance, editing=True)
self.editing = True
instance = self.get_instance()
form = self.make_form(instance)
if self.request.method == 'POST':
if form.validate():
form.save()
@ -112,10 +113,8 @@ class MasterView(View):
"""
View for deleting an existing model record.
"""
key = self.request.matchdict[self.get_model_key()]
instance = Session.query(self.model_class).get(key)
if not instance:
return HTTPNotFound()
self.deleting = True
instance = self.get_instance()
# Let derived classes prep for (or cancel) deletion.
result = self.before_delete(instance)
@ -253,6 +252,12 @@ class MasterView(View):
return render_to_response('/master/{0}.mako'.format(template),
data, request=self.request)
def template_kwargs(self, **kwargs):
"""
Supplement the template context, for all views.
"""
return kwargs
def redirect(self, url):
"""
Convenience method to return a HTTP 302 response.
@ -362,29 +367,40 @@ class MasterView(View):
def make_query(self, session=None):
"""
Make the base query to be used for the grid. Note that this query will
have been prefiltered but otherwise will be "pure". The user's filter
selections etc. are later applied to this query.
Make the base query to be used for the grid. Subclasses should not
override this method; override :meth:`query()` instead.
"""
if session is None:
session = Session()
query = session.query(self.model_class)
return self.prefilter_query(query)
return self.query(session)
def prefilter_query(self, query):
def query(self, session):
"""
Apply any sort of pre-filtering to the grid query, as necessary. This
is useful if say, you don't ever want to show records of a certain type
to non-admin users. You would use a "prefilter" to hide what you
wanted, regardless of the user's filter selections.
Produce the initial/base query for the master grid. By default this is
simply a query against the model class, but you may override as
necessary to apply any sort of pre-filtering etc. This is useful if
say, you don't ever want to show records of a certain type to non-admin
users. You would modify the base query to hide what you wanted,
regardless of the user's filter selections.
"""
return query
return session.query(self.model_class)
##############################
# CRUD Stuff
##############################
def get_instance(self):
"""
Fetch the current model instance by inspecting the route kwargs and
doing a database lookup. If the instance cannot be found, raises 404.
"""
key = self.request.matchdict[self.get_model_key()]
instance = Session.query(self.model_class).get(key)
if not instance:
raise HTTPNotFound()
return instance
def make_form(self, instance, **kwargs):
"""
Make a FormAlchemy-based form for use with CRUD views.
@ -392,13 +408,8 @@ class MasterView(View):
# TODO: Some hacky stuff here, to accommodate old form cruft. Probably
# should refactor forms soon too, but trying to avoid it for the moment.
readonly = kwargs.pop('readonly', False)
kwargs.setdefault('creating', False)
kwargs.setdefault('editing', False)
# Ugh, these attributes must be present on the view..?
self.creating = kwargs['creating']
self.editing = kwargs['editing']
kwargs.setdefault('creating', self.creating)
kwargs.setdefault('editing', self.editing)
fieldset = self.make_fieldset(instance)
self.configure_fieldset(fieldset)
@ -409,7 +420,7 @@ class MasterView(View):
else:
kwargs.setdefault('cancel_url', self.get_action_url('view', instance))
form = AlchemyForm(self.request, fieldset, **kwargs)
form.readonly = readonly
form.readonly = self.viewing
return form
def make_fieldset(self, instance, **kwargs):
@ -422,11 +433,10 @@ class MasterView(View):
fieldset.prettify = prettify
return fieldset
def template_kwargs(self, **kwargs):
def before_delete(self, instance):
"""
Supplement the template context, for all views.
Event hook which is called just before deletion is attempted.
"""
return kwargs
##############################
# Config Stuff

View file

@ -134,7 +134,7 @@ class RolesView(MasterView):
users = AlchemyGrid('roles.users', self.request, data=users, model_class=model.User,
main_actions=[
GridAction('view', icon='zoomin',
url=lambda u: self.request.route_url('user.read', uuid=u.uuid)),
url=lambda u: self.request.route_url('users.view', uuid=u.uuid)),
])
users.configure(include=[users.username], readonly=True)
kwargs['users'] = users

View file

@ -26,125 +26,28 @@ User Views
from __future__ import unicode_literals
from sqlalchemy import orm
from rattail.db import model
from rattail.db.model import User, Person, Role
from rattail.db.auth import guest_role, set_user_password
import formalchemy
from formalchemy import Field, ValidationError
from formalchemy.fields import SelectFieldRenderer
from webhelpers.html import tags
from webhelpers.html import HTML
from webhelpers.html import HTML, tags
from . import SearchableAlchemyGridView, CrudView
from ..forms import PersonFieldLinkRenderer
from ..db import Session
from tailbone.grids.search import BooleanSearchFilter
from .continuum import VersionView, version_defaults
from tailbone.db import Session
from tailbone.views import MasterView
from tailbone.views.continuum import VersionView, version_defaults
from tailbone.forms import renderers
class UsersGrid(SearchableAlchemyGridView):
mapped_class = User
config_prefix = 'users'
sort = 'username'
def join_map(self):
return {
'person':
lambda q: q.outerjoin(Person),
}
def filter_map(self):
return self.make_filter_map(
ilike=['username'],
exact=['active'],
person=self.filter_ilike(Person.display_name))
def filter_config(self):
return self.make_filter_config(
include_filter_username=True,
filter_type_username='lk',
include_filter_person=True,
filter_type_person='lk',
filter_factory_active=BooleanSearchFilter,
include_filter_active=True,
filter_type_active='is',
active='True')
def sort_map(self):
return self.make_sort_map(
'username',
person=self.sorter(Person.display_name))
def grid(self):
g = self.make_grid()
g.configure(
include=[
g.username,
g.person,
],
readonly=True)
if self.request.has_perm('users.read'):
g.viewable = True
g.view_route_name = 'user.read'
if self.request.has_perm('users.update'):
g.editable = True
g.edit_route_name = 'user.update'
if self.request.has_perm('users.delete'):
g.deletable = True
g.delete_route_name = 'user.delete'
return g
class RolesField(Field):
def __init__(self, name, **kwargs):
kwargs.setdefault('value', self.get_value)
kwargs.setdefault('options', self.get_options())
kwargs.setdefault('multiple', True)
super(RolesField, self).__init__(name, **kwargs)
def get_value(self, user):
return [x.uuid for x in user.roles]
def get_options(self):
return Session.query(Role.name, Role.uuid)\
.filter(Role.uuid != guest_role(Session()).uuid)\
.order_by(Role.name)\
.all()
def sync(self):
if not self.is_readonly():
user = self.model
roles = Session.query(Role)
data = self.renderer.deserialize()
user.roles = [roles.get(x) for x in data]
def RolesFieldRenderer(request):
class RolesFieldRenderer(SelectFieldRenderer):
def render_readonly(self, **kwargs):
roles = Session.query(Role)
html = ''
for uuid in self.value:
role = roles.get(uuid)
link = tags.link_to(
role.name, request.route_url('roles.view', uuid=role.uuid))
html += HTML.tag('li', c=link)
html = HTML.tag('ul', c=html)
return html
return RolesFieldRenderer
class PasswordFieldRenderer(formalchemy.PasswordFieldRenderer):
def render(self, **kwargs):
return tags.password(self.name, value='', maxlength=self.length, **kwargs)
def unique_username(value, field):
user = field.parent.model
query = Session.query(model.User).filter(model.User.username == value)
if user.uuid:
query = query.filter(model.User.uuid != user.uuid)
if query.count():
raise formalchemy.ValidationError("Username must be unique.")
def passwords_match(value, field):
@ -153,6 +56,12 @@ def passwords_match(value, field):
return value
class PasswordFieldRenderer(formalchemy.PasswordFieldRenderer):
def render(self, **kwargs):
return tags.password(self.name, value='', maxlength=self.length, **kwargs)
class PasswordField(formalchemy.Field):
def __init__(self, *args, **kwargs):
@ -168,45 +77,103 @@ class PasswordField(formalchemy.Field):
set_user_password(self.model, password)
class UserCrud(CrudView):
def RolesFieldRenderer(request):
mapped_class = User
home_route = 'users'
class RolesFieldRenderer(SelectFieldRenderer):
def fieldset(self, user):
fs = self.make_fieldset(user)
def render_readonly(self, **kwargs):
roles = Session.query(model.Role)
html = ''
for uuid in self.value:
role = roles.get(uuid)
link = tags.link_to(
role.name, request.route_url('roles.view', uuid=role.uuid))
html += HTML.tag('li', c=link)
html = HTML.tag('ul', c=html)
return html
# Must set Person options to empty set to avoid unwanted magic.
return RolesFieldRenderer
class RolesField(formalchemy.Field):
def __init__(self, name, **kwargs):
kwargs.setdefault('value', self.get_value)
kwargs.setdefault('options', self.get_options())
kwargs.setdefault('multiple', True)
super(RolesField, self).__init__(name, **kwargs)
def get_value(self, user):
return [x.uuid for x in user.roles]
def get_options(self):
return Session.query(model.Role.name, model.Role.uuid)\
.filter(model.Role.uuid != guest_role(Session()).uuid)\
.order_by(model.Role.name)\
.all()
def sync(self):
if not self.is_readonly():
user = self.model
roles = Session.query(model.Role)
data = self.renderer.deserialize()
user.roles = [roles.get(x) for x in data]
class UsersView(MasterView):
"""
Master view for the User model.
"""
model_class = model.User
def query(self, session):
return session.query(model.User)\
.options(orm.joinedload(model.User.person))
def configure_grid(self, g):
g.joiners['person'] = lambda q: q.outerjoin(model.Person)
del g.filters['password']
del g.filters['salt']
g.filters['username'].default_active = True
g.filters['username'].default_verb = 'contains'
g.filters['active'].verbs = ['is_true', 'is_false', 'is_any']
g.filters['active'].default_active = True
g.filters['active'].default_verb = 'is_true'
g.filters['person'] = g.make_filter('person', model.Person.display_name, label="Person's Name",
default_active=True, default_verb='contains')
g.sorters['person'] = lambda q, d: q.order_by(getattr(model.Person.display_name, d)())
g.default_sortkey = 'username'
g.person.set(label="Person's Name")
g.configure(
include=[
g.username,
g.person,
],
readonly=True)
def configure_fieldset(self, fs):
fs.username.set(validate=unique_username)
fs.person.set(options=[])
fs.append(PasswordField('password'))
fs.append(Field('confirm_password',
renderer=PasswordFieldRenderer))
fs.append(RolesField(
'roles', renderer=RolesFieldRenderer(self.request)))
fs.person.set(renderer=renderers.PersonFieldLinkRenderer)
fs.append(PasswordField('password', label="Set Password"))
fs.append(formalchemy.Field('confirm_password', renderer=PasswordFieldRenderer))
fs.append(RolesField('roles', renderer=RolesFieldRenderer(self.request)))
fs.configure(
include=[
fs.username,
fs.person.with_renderer(PersonFieldLinkRenderer),
fs.password.label("Set Password"),
fs.person,
fs.active,
fs.password,
fs.confirm_password,
fs.roles,
fs.active,
])
if self.creating:
def unique_username(value, field):
if Session.query(User).filter_by(username=value).count():
raise ValidationError("Username must be unique.")
fs.username.set(validate=unique_username)
if self.readonly:
])
if self.viewing:
del fs.password
del fs.confirm_password
return fs
class UserVersionView(VersionView):
"""
@ -216,33 +183,6 @@ class UserVersionView(VersionView):
route_model_view = 'user.read'
def add_routes(config):
config.add_route(u'users', u'/users')
config.add_route(u'user.create', u'/users/new')
config.add_route(u'user.read', u'/users/{uuid}')
config.add_route(u'user.update', u'/users/{uuid}/edit')
config.add_route(u'user.delete', u'/users/{uuid}/delete')
def includeme(config):
add_routes(config)
# List
config.add_view(UsersGrid, route_name='users',
renderer='/users/index.mako',
permission='users.list')
# CRUD
config.add_view(UserCrud, attr='create', route_name='user.create',
renderer='/users/crud.mako',
permission='users.create')
config.add_view(UserCrud, attr='read', route_name='user.read',
renderer='/users/crud.mako',
permission='users.read')
config.add_view(UserCrud, attr='update', route_name='user.update',
renderer='/users/crud.mako',
permission='users.update')
config.add_view(UserCrud, attr='delete', route_name='user.delete',
permission='users.delete')
UsersView.defaults(config)
version_defaults(config, UserVersionView, 'user')