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

@ -29,6 +29,8 @@ from __future__ import unicode_literals
import os import os
import logging import logging
import sqlalchemy as sa
import edbob import edbob
from edbob.pyramid.forms.formalchemy import TemplateEngine from edbob.pyramid.forms.formalchemy import TemplateEngine
@ -131,6 +133,7 @@ def make_pyramid_config(settings):
# Configure FormAlchemy. # Configure FormAlchemy.
formalchemy.config.engine = TemplateEngine() formalchemy.config.engine = TemplateEngine()
formalchemy.FieldSet.default_renderers[sa.Boolean] = renderers.YesNoFieldRenderer
formalchemy.FieldSet.default_renderers[GPCType] = renderers.GPCFieldRenderer formalchemy.FieldSet.default_renderers[GPCType] = renderers.GPCFieldRenderer
return config return config

View file

@ -78,26 +78,22 @@ class AlchemyGrid(Grid):
filtrs = filters.GridFilterSet() filtrs = filters.GridFilterSet()
mapper = orm.class_mapper(self.model_class) mapper = orm.class_mapper(self.model_class)
for prop in mapper.iterate_properties: for prop in mapper.iterate_properties:
if isinstance(prop, orm.ColumnProperty) and prop.key != 'uuid': if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
filtrs[prop.key] = self.make_filter(prop) filtrs[prop.key] = self.make_filter(prop.key, prop.columns[0])
return filtrs return filtrs
def make_filter(self, model_property, **kwargs): def make_filter(self, key, column, **kwargs):
""" """
Make a filter suitable for use with the given model property. Make a filter suitable for use with the given column.
""" """
if len(model_property.columns) > 1:
log.debug("ignoring secondary columns for sake of type detection")
coltype = model_property.columns[0].type
factory = filters.AlchemyGridFilter factory = filters.AlchemyGridFilter
if isinstance(coltype, sa.String): if isinstance(column.type, sa.String):
factory = filters.AlchemyStringFilter factory = filters.AlchemyStringFilter
elif isinstance(coltype, sa.Numeric): elif isinstance(column.type, sa.Numeric):
factory = filters.AlchemyNumericFilter factory = filters.AlchemyNumericFilter
elif isinstance(column.type, sa.Boolean):
kwargs['model_property'] = model_property factory = filters.AlchemyBooleanFilter
return factory(model_property.key, **kwargs) return factory(key, column=column, **kwargs)
def iter_filters(self): def iter_filters(self):
""" """
@ -112,19 +108,20 @@ class AlchemyGrid(Grid):
""" """
sorters, updates = {}, sorters sorters, updates = {}, sorters
mapper = orm.class_mapper(self.model_class) mapper = orm.class_mapper(self.model_class)
for key, column in mapper.columns.items(): for prop in mapper.iterate_properties:
if key != 'uuid': if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
sorters[key] = self.make_sorter(column) sorters[prop.key] = self.make_sorter(prop)
if updates: if updates:
sorters.update(updates) sorters.update(updates)
return sorters return sorters
def make_sorter(self, field): def make_sorter(self, model_property):
""" """
Returns a function suitable for a sort map callable, with typical logic Returns a function suitable for a sort map callable, with typical logic
built in for sorting applied to ``field``. built in for sorting applied to ``field``.
""" """
return lambda q, d: q.order_by(getattr(field, d)()) column = getattr(self.model_class, model_property.key)
return lambda q, d: q.order_by(getattr(column, d)())
def load_settings(self): def load_settings(self):
""" """
@ -136,23 +133,6 @@ class AlchemyGrid(Grid):
self._fa_grid.rebind(self.make_visible_data(), session=Session(), self._fa_grid.rebind(self.make_visible_data(), session=Session(),
request=self.request) request=self.request)
def sort_data(self, query):
"""
Sort the given query according to current settings, and return the result.
"""
# Cannot sort unless we know which column to sort by.
if not self.sortkey:
return query
# Cannot sort unless we have a sort function.
sortfunc = self.sorters.get(self.sortkey)
if not sortfunc:
return query
# We can provide a default sort direction though.
sortdir = getattr(self, 'sortdir', 'asc')
return sortfunc(query, sortdir)
def paginate_data(self, query): def paginate_data(self, query):
""" """
Paginate the given data set according to current settings, and return Paginate the given data set according to current settings, and return
@ -170,7 +150,7 @@ class AlchemyGrid(Grid):
for field in self._fa_grid.render_fields.itervalues(): for field in self._fa_grid.render_fields.itervalues():
column = GridColumn() column = GridColumn()
column.field = field column.field = field
column.key = field.name column.key = field.key
column.label = field.label() column.label = field.label()
yield column yield column

View file

@ -39,7 +39,7 @@ class Grid(object):
""" """
def __init__(self, key, request, columns=[], data=[], main_actions=[], more_actions=[], def __init__(self, key, request, columns=[], data=[], main_actions=[], more_actions=[],
filterable=False, filters={}, joiners={}, filterable=False, filters={},
sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc', sortable=False, sorters={}, default_sortkey=None, default_sortdir='asc',
pageable=False, default_pagesize=20, default_page=1, pageable=False, default_pagesize=20, default_page=1,
width='auto', checkboxes=False, **kwargs): width='auto', checkboxes=False, **kwargs):
@ -49,6 +49,7 @@ class Grid(object):
self.data = data self.data = data
self.main_actions = main_actions self.main_actions = main_actions
self.more_actions = more_actions self.more_actions = more_actions
self.joiners = joiners
# Set extra attributes first, in case other init logic depends on any # Set extra attributes first, in case other init logic depends on any
# of them (i.e. in subclasses). # of them (i.e. in subclasses).
@ -378,15 +379,31 @@ class Grid(object):
Filter and return the given data set, according to current settings. Filter and return the given data set, according to current settings.
""" """
for filtr in self.iter_active_filters(): for filtr in self.iter_active_filters():
if filtr.key in self.joiners and filtr.key not in self.joined:
data = self.joiners[filtr.key](data)
self.joined.add(filtr.key)
data = filtr.filter(data) data = filtr.filter(data)
return data return data
def sort_data(self, data): def sort_data(self, data):
""" """
Sort the given data set according to current settings, and return the Sort the given query according to current settings, and return the result.
result. Note that the default implementation does nothing.
""" """
return data # Cannot sort unless we know which column to sort by.
if not self.sortkey:
return data
# Cannot sort unless we have a sort function.
sortfunc = self.sorters.get(self.sortkey)
if not sortfunc:
return data
# We can provide a default sort direction though.
sortdir = getattr(self, 'sortdir', 'asc')
if self.sortkey in self.joiners and self.sortkey not in self.joined:
data = self.joiners[self.sortkey](data)
self.joined.add(self.sortkey)
return sortfunc(data, sortdir)
def paginate_data(self, data): def paginate_data(self, data):
""" """
@ -401,6 +418,7 @@ class Grid(object):
set. This will page / sort / filter as necessary, according to the set. This will page / sort / filter as necessary, according to the
grid's defaults and the current request etc. grid's defaults and the current request etc.
""" """
self.joined = set()
data = self.data data = self.data
if self.filterable: if self.filterable:
data = self.filter_data(data) data = self.filter_data(data)

View file

@ -70,6 +70,7 @@ class GridFilter(object):
'filters' section when rendering the index page template. 'filters' section when rendering the index page template.
""" """
verbmap = { verbmap = {
'is_any': "is any",
'equal': "equal to", 'equal': "equal to",
'not_equal': "not equal to", 'not_equal': "not equal to",
'greater_than': "greater than", 'greater_than': "greater than",
@ -78,6 +79,8 @@ class GridFilter(object):
'less_equal': "less than or equal to", 'less_equal': "less than or equal to",
'is_null': "is null", 'is_null': "is null",
'is_not_null': "is not null", 'is_not_null': "is not null",
'is_true': "is true",
'is_false': "is false",
'contains': "contains", 'contains': "contains",
'does_not_contain': "does not contain", 'does_not_contain': "does not contain",
} }
@ -86,7 +89,7 @@ class GridFilter(object):
default_active=False, default_verb=None, default_value=None): default_active=False, default_verb=None, default_value=None):
self.key = key self.key = key
self.label = label or prettify(key) self.label = label or prettify(key)
self.verbs = verbs or self.default_verbs() self.verbs = verbs or self.get_default_verbs()
self.renderer = renderer or DefaultRenderer() self.renderer = renderer or DefaultRenderer()
self.renderer.filter = self self.renderer.filter = self
self.default_active = default_active self.default_active = default_active
@ -96,11 +99,16 @@ class GridFilter(object):
def __repr__(self): def __repr__(self):
return "GridFilter({0})".format(repr(self.key)) return "GridFilter({0})".format(repr(self.key))
def default_verbs(self): def get_default_verbs(self):
""" """
Returns the set of verbs which will be used by default, i.e. unless Returns the set of verbs which will be used by default, i.e. unless
overridden by constructor args etc. overridden by constructor args etc.
""" """
verbs = getattr(self, 'default_verbs', None)
if verbs:
if callable(verbs):
return verbs()
return verbs
return ['equal', 'not_equal', 'is_null', 'is_not_null'] return ['equal', 'not_equal', 'is_null', 'is_not_null']
def filter(self, data, verb=None, value=UNSPECIFIED): def filter(self, data, verb=None, value=UNSPECIFIED):
@ -116,6 +124,14 @@ class GridFilter(object):
raise ValueError("Unknown filter verb: {0}".format(repr(verb))) raise ValueError("Unknown filter verb: {0}".format(repr(verb)))
return filtr(data, value) return filtr(data, value)
def filter_is_any(self, data, value):
"""
Special no-op filter which does no actual filtering. Useful in some
cases to add an "ineffective" option to the verb list for a given grid
filter.
"""
return data
def render(self, **kwargs): def render(self, **kwargs):
kwargs['filter'] = self kwargs['filter'] = self
return self.renderer.render(**kwargs) return self.renderer.render(**kwargs)
@ -127,9 +143,7 @@ class AlchemyGridFilter(GridFilter):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.model_property = kwargs.pop('model_property') self.column = kwargs.pop('column')
self.model_class = self.model_property.parent.class_
self.model_column = getattr(self.model_class, self.model_property.key)
super(AlchemyGridFilter, self).__init__(*args, **kwargs) super(AlchemyGridFilter, self).__init__(*args, **kwargs)
def filter_equal(self, query, value): def filter_equal(self, query, value):
@ -138,7 +152,7 @@ class AlchemyGridFilter(GridFilter):
""" """
if value is None or value == '': if value is None or value == '':
return query return query
return query.filter(self.model_column == value) return query.filter(self.column == value)
def filter_not_equal(self, query, value): def filter_not_equal(self, query, value):
""" """
@ -150,8 +164,8 @@ class AlchemyGridFilter(GridFilter):
# When saying something is 'not equal' to something else, we must also # When saying something is 'not equal' to something else, we must also
# include things which are nothing at all, in our result set. # include things which are nothing at all, in our result set.
return query.filter(sa.or_( return query.filter(sa.or_(
self.model_column == None, self.column == None,
self.model_column != value, self.column != value,
)) ))
def filter_is_null(self, query, value): def filter_is_null(self, query, value):
@ -159,14 +173,14 @@ class AlchemyGridFilter(GridFilter):
Filter data with an 'IS NULL' query. Note that this filter does not Filter data with an 'IS NULL' query. Note that this filter does not
use the value for anything. use the value for anything.
""" """
return query.filter(self.model_column == None) return query.filter(self.column == None)
def filter_is_not_null(self, query, value): def filter_is_not_null(self, query, value):
""" """
Filter data with an 'IS NOT NULL' query. Note that this filter does Filter data with an 'IS NOT NULL' query. Note that this filter does
not use the value for anything. not use the value for anything.
""" """
return query.filter(self.model_column != None) return query.filter(self.column != None)
def filter_greater_than(self, query, value): def filter_greater_than(self, query, value):
""" """
@ -174,7 +188,7 @@ class AlchemyGridFilter(GridFilter):
""" """
if value is None or value == '': if value is None or value == '':
return query return query
return query.filter(self.model_column > value) return query.filter(self.column > value)
def filter_greater_equal(self, query, value): def filter_greater_equal(self, query, value):
""" """
@ -182,7 +196,7 @@ class AlchemyGridFilter(GridFilter):
""" """
if value is None or value == '': if value is None or value == '':
return query return query
return query.filter(self.model_column >= value) return query.filter(self.column >= value)
def filter_less_than(self, query, value): def filter_less_than(self, query, value):
""" """
@ -190,7 +204,7 @@ class AlchemyGridFilter(GridFilter):
""" """
if value is None or value == '': if value is None or value == '':
return query return query
return query.filter(self.model_column < value) return query.filter(self.column < value)
def filter_less_equal(self, query, value): def filter_less_equal(self, query, value):
""" """
@ -198,7 +212,7 @@ class AlchemyGridFilter(GridFilter):
""" """
if value is None or value == '': if value is None or value == '':
return query return query
return query.filter(self.model_column <= value) return query.filter(self.column <= value)
class AlchemyStringFilter(AlchemyGridFilter): class AlchemyStringFilter(AlchemyGridFilter):
@ -219,7 +233,7 @@ class AlchemyStringFilter(AlchemyGridFilter):
""" """
if value is None or value == '': if value is None or value == '':
return query return query
return query.filter(self.model_column.ilike('%{0}%'.format(value))) return query.filter(self.column.ilike('%{0}%'.format(value)))
def filter_does_not_contain(self, query, value): def filter_does_not_contain(self, query, value):
""" """
@ -231,8 +245,8 @@ class AlchemyStringFilter(AlchemyGridFilter):
# When saying something is 'not like' something else, we must also # When saying something is 'not like' something else, we must also
# include things which are nothing at all, in our result set. # include things which are nothing at all, in our result set.
return query.filter(sa.or_( return query.filter(sa.or_(
self.model_column == None, self.column == None,
~self.model_column.ilike('%{0}%'.format(value)), ~self.column.ilike('%{0}%'.format(value)),
)) ))
@ -249,6 +263,27 @@ class AlchemyNumericFilter(AlchemyGridFilter):
'less_than', 'less_equal', 'is_null', 'is_not_null'] 'less_than', 'less_equal', 'is_null', 'is_not_null']
class AlchemyBooleanFilter(AlchemyGridFilter):
"""
Boolean filter for SQLAlchemy.
"""
default_verbs = ['is_true', 'is_false', 'is_null', 'is_not_null', 'is_any']
def filter_is_true(self, query, value):
"""
Filter data with an "is true" query. Note that this filter does not
use the value for anything.
"""
return query.filter(self.column == True)
def filter_is_false(self, query, value):
"""
Filter data with an "is false" query. Note that this filter does not
use the value for anything.
"""
return query.filter(self.column == False)
class GridFilterSet(OrderedDict): class GridFilterSet(OrderedDict):
""" """
Collection class for :class:`GridFilter` instances. Collection class for :class:`GridFilter` instances.

View file

@ -1,16 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/crud.mako" />
<%def name="context_menu_items()">
<li>${h.link_to("Back to Users", url('users'))}</li>
% if form.readonly:
<li>${h.link_to("Edit this User", url('user.update', uuid=form.fieldset.model.uuid))}</li>
% elif form.updating:
<li>${h.link_to("View this User", url('user.read', uuid=form.fieldset.model.uuid))}</li>
% endif
% if version_count is not Undefined and request.has_perm('user.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=form.fieldset.model.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/edit.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if version_count is not Undefined and request.has_perm('user.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

@ -1,12 +0,0 @@
## -*- coding: utf-8 -*-
<%inherit file="/grid.mako" />
<%def name="title()">Users</%def>
<%def name="context_menu_items()">
% if request.has_perm('users.create'):
<li>${h.link_to("Create a new User", url('user.create'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/view.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if version_count is not Undefined and request.has_perm('user.versions.view'):
<li>${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=instance.uuid))}</li>
% endif
</%def>
${parent.body()}

View file

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

View file

@ -134,7 +134,7 @@ class RolesView(MasterView):
users = AlchemyGrid('roles.users', self.request, data=users, model_class=model.User, users = AlchemyGrid('roles.users', self.request, data=users, model_class=model.User,
main_actions=[ main_actions=[
GridAction('view', icon='zoomin', 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) users.configure(include=[users.username], readonly=True)
kwargs['users'] = users kwargs['users'] = users

View file

@ -26,125 +26,28 @@ User Views
from __future__ import unicode_literals from __future__ import unicode_literals
from sqlalchemy import orm
from rattail.db import model from rattail.db import model
from rattail.db.model import User, Person, Role
from rattail.db.auth import guest_role, set_user_password from rattail.db.auth import guest_role, set_user_password
import formalchemy import formalchemy
from formalchemy import Field, ValidationError
from formalchemy.fields import SelectFieldRenderer from formalchemy.fields import SelectFieldRenderer
from webhelpers.html import tags from webhelpers.html import HTML, tags
from webhelpers.html import HTML
from . import SearchableAlchemyGridView, CrudView from tailbone.db import Session
from ..forms import PersonFieldLinkRenderer from tailbone.views import MasterView
from ..db import Session from tailbone.views.continuum import VersionView, version_defaults
from tailbone.grids.search import BooleanSearchFilter from tailbone.forms import renderers
from .continuum import VersionView, version_defaults
class UsersGrid(SearchableAlchemyGridView): def unique_username(value, field):
user = field.parent.model
mapped_class = User query = Session.query(model.User).filter(model.User.username == value)
config_prefix = 'users' if user.uuid:
sort = 'username' query = query.filter(model.User.uuid != user.uuid)
if query.count():
def join_map(self): raise formalchemy.ValidationError("Username must be unique.")
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 passwords_match(value, field): def passwords_match(value, field):
@ -153,6 +56,12 @@ def passwords_match(value, field):
return value return value
class PasswordFieldRenderer(formalchemy.PasswordFieldRenderer):
def render(self, **kwargs):
return tags.password(self.name, value='', maxlength=self.length, **kwargs)
class PasswordField(formalchemy.Field): class PasswordField(formalchemy.Field):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -168,45 +77,103 @@ class PasswordField(formalchemy.Field):
set_user_password(self.model, password) set_user_password(self.model, password)
class UserCrud(CrudView): def RolesFieldRenderer(request):
mapped_class = User class RolesFieldRenderer(SelectFieldRenderer):
home_route = 'users'
def fieldset(self, user): def render_readonly(self, **kwargs):
fs = self.make_fieldset(user) 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.person.set(options=[])
fs.person.set(renderer=renderers.PersonFieldLinkRenderer)
fs.append(PasswordField('password')) fs.append(PasswordField('password', label="Set Password"))
fs.append(Field('confirm_password', fs.append(formalchemy.Field('confirm_password', renderer=PasswordFieldRenderer))
renderer=PasswordFieldRenderer)) fs.append(RolesField('roles', renderer=RolesFieldRenderer(self.request)))
fs.append(RolesField(
'roles', renderer=RolesFieldRenderer(self.request)))
fs.configure( fs.configure(
include=[ include=[
fs.username, fs.username,
fs.person.with_renderer(PersonFieldLinkRenderer), fs.person,
fs.password.label("Set Password"), fs.active,
fs.password,
fs.confirm_password, fs.confirm_password,
fs.roles, fs.roles,
fs.active, ])
]) if self.viewing:
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:
del fs.password del fs.password
del fs.confirm_password del fs.confirm_password
return fs
class UserVersionView(VersionView): class UserVersionView(VersionView):
""" """
@ -216,33 +183,6 @@ class UserVersionView(VersionView):
route_model_view = 'user.read' 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): def includeme(config):
add_routes(config) UsersView.defaults(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')
version_defaults(config, UserVersionView, 'user') version_defaults(config, UserVersionView, 'user')