Convert User pages to use master view.
And of course make some more tweaks to new grids etc.
This commit is contained in:
parent
9cfbc918e7
commit
af07f477dc
|
@ -29,6 +29,8 @@ from __future__ import unicode_literals
|
|||
import os
|
||||
import logging
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import edbob
|
||||
from edbob.pyramid.forms.formalchemy import TemplateEngine
|
||||
|
||||
|
@ -131,6 +133,7 @@ def make_pyramid_config(settings):
|
|||
|
||||
# Configure FormAlchemy.
|
||||
formalchemy.config.engine = TemplateEngine()
|
||||
formalchemy.FieldSet.default_renderers[sa.Boolean] = renderers.YesNoFieldRenderer
|
||||
formalchemy.FieldSet.default_renderers[GPCType] = renderers.GPCFieldRenderer
|
||||
|
||||
return config
|
||||
|
|
|
@ -78,26 +78,22 @@ class AlchemyGrid(Grid):
|
|||
filtrs = filters.GridFilterSet()
|
||||
mapper = orm.class_mapper(self.model_class)
|
||||
for prop in mapper.iterate_properties:
|
||||
if isinstance(prop, orm.ColumnProperty) and prop.key != 'uuid':
|
||||
filtrs[prop.key] = self.make_filter(prop)
|
||||
if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
|
||||
filtrs[prop.key] = self.make_filter(prop.key, prop.columns[0])
|
||||
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
|
||||
if isinstance(coltype, sa.String):
|
||||
if isinstance(column.type, sa.String):
|
||||
factory = filters.AlchemyStringFilter
|
||||
elif isinstance(coltype, sa.Numeric):
|
||||
elif isinstance(column.type, sa.Numeric):
|
||||
factory = filters.AlchemyNumericFilter
|
||||
|
||||
kwargs['model_property'] = model_property
|
||||
return factory(model_property.key, **kwargs)
|
||||
elif isinstance(column.type, sa.Boolean):
|
||||
factory = filters.AlchemyBooleanFilter
|
||||
return factory(key, column=column, **kwargs)
|
||||
|
||||
def iter_filters(self):
|
||||
"""
|
||||
|
@ -112,19 +108,20 @@ class AlchemyGrid(Grid):
|
|||
"""
|
||||
sorters, updates = {}, sorters
|
||||
mapper = orm.class_mapper(self.model_class)
|
||||
for key, column in mapper.columns.items():
|
||||
if key != 'uuid':
|
||||
sorters[key] = self.make_sorter(column)
|
||||
for prop in mapper.iterate_properties:
|
||||
if isinstance(prop, orm.ColumnProperty) and not prop.key.endswith('uuid'):
|
||||
sorters[prop.key] = self.make_sorter(prop)
|
||||
if updates:
|
||||
sorters.update(updates)
|
||||
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
|
||||
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):
|
||||
"""
|
||||
|
@ -136,23 +133,6 @@ class AlchemyGrid(Grid):
|
|||
self._fa_grid.rebind(self.make_visible_data(), session=Session(),
|
||||
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):
|
||||
"""
|
||||
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():
|
||||
column = GridColumn()
|
||||
column.field = field
|
||||
column.key = field.name
|
||||
column.key = field.key
|
||||
column.label = field.label()
|
||||
yield column
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ class Grid(object):
|
|||
"""
|
||||
|
||||
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',
|
||||
pageable=False, default_pagesize=20, default_page=1,
|
||||
width='auto', checkboxes=False, **kwargs):
|
||||
|
@ -49,6 +49,7 @@ class Grid(object):
|
|||
self.data = data
|
||||
self.main_actions = main_actions
|
||||
self.more_actions = more_actions
|
||||
self.joiners = joiners
|
||||
|
||||
# Set extra attributes first, in case other init logic depends on any
|
||||
# of them (i.e. in subclasses).
|
||||
|
@ -378,16 +379,32 @@ class Grid(object):
|
|||
Filter and return the given data set, according to current settings.
|
||||
"""
|
||||
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)
|
||||
return data
|
||||
|
||||
def sort_data(self, data):
|
||||
"""
|
||||
Sort the given data set according to current settings, and return the
|
||||
result. Note that the default implementation does nothing.
|
||||
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 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):
|
||||
"""
|
||||
Paginate the given data set according to current settings, and return
|
||||
|
@ -401,6 +418,7 @@ class Grid(object):
|
|||
set. This will page / sort / filter as necessary, according to the
|
||||
grid's defaults and the current request etc.
|
||||
"""
|
||||
self.joined = set()
|
||||
data = self.data
|
||||
if self.filterable:
|
||||
data = self.filter_data(data)
|
||||
|
|
|
@ -70,6 +70,7 @@ class GridFilter(object):
|
|||
'filters' section when rendering the index page template.
|
||||
"""
|
||||
verbmap = {
|
||||
'is_any': "is any",
|
||||
'equal': "equal to",
|
||||
'not_equal': "not equal to",
|
||||
'greater_than': "greater than",
|
||||
|
@ -78,6 +79,8 @@ class GridFilter(object):
|
|||
'less_equal': "less than or equal to",
|
||||
'is_null': "is null",
|
||||
'is_not_null': "is not null",
|
||||
'is_true': "is true",
|
||||
'is_false': "is false",
|
||||
'contains': "contains",
|
||||
'does_not_contain': "does not contain",
|
||||
}
|
||||
|
@ -86,7 +89,7 @@ class GridFilter(object):
|
|||
default_active=False, default_verb=None, default_value=None):
|
||||
self.key = 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.filter = self
|
||||
self.default_active = default_active
|
||||
|
@ -96,11 +99,16 @@ class GridFilter(object):
|
|||
def __repr__(self):
|
||||
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
|
||||
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']
|
||||
|
||||
def filter(self, data, verb=None, value=UNSPECIFIED):
|
||||
|
@ -116,6 +124,14 @@ class GridFilter(object):
|
|||
raise ValueError("Unknown filter verb: {0}".format(repr(verb)))
|
||||
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):
|
||||
kwargs['filter'] = self
|
||||
return self.renderer.render(**kwargs)
|
||||
|
@ -127,9 +143,7 @@ class AlchemyGridFilter(GridFilter):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.model_property = kwargs.pop('model_property')
|
||||
self.model_class = self.model_property.parent.class_
|
||||
self.model_column = getattr(self.model_class, self.model_property.key)
|
||||
self.column = kwargs.pop('column')
|
||||
super(AlchemyGridFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter_equal(self, query, value):
|
||||
|
@ -138,7 +152,7 @@ class AlchemyGridFilter(GridFilter):
|
|||
"""
|
||||
if value is None or value == '':
|
||||
return query
|
||||
return query.filter(self.model_column == value)
|
||||
return query.filter(self.column == 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
|
||||
# include things which are nothing at all, in our result set.
|
||||
return query.filter(sa.or_(
|
||||
self.model_column == None,
|
||||
self.model_column != value,
|
||||
self.column == None,
|
||||
self.column != 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
|
||||
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):
|
||||
"""
|
||||
Filter data with an 'IS NOT NULL' query. Note that this filter does
|
||||
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):
|
||||
"""
|
||||
|
@ -174,7 +188,7 @@ class AlchemyGridFilter(GridFilter):
|
|||
"""
|
||||
if value is None or value == '':
|
||||
return query
|
||||
return query.filter(self.model_column > value)
|
||||
return query.filter(self.column > value)
|
||||
|
||||
def filter_greater_equal(self, query, value):
|
||||
"""
|
||||
|
@ -182,7 +196,7 @@ class AlchemyGridFilter(GridFilter):
|
|||
"""
|
||||
if value is None or value == '':
|
||||
return query
|
||||
return query.filter(self.model_column >= value)
|
||||
return query.filter(self.column >= value)
|
||||
|
||||
def filter_less_than(self, query, value):
|
||||
"""
|
||||
|
@ -190,7 +204,7 @@ class AlchemyGridFilter(GridFilter):
|
|||
"""
|
||||
if value is None or value == '':
|
||||
return query
|
||||
return query.filter(self.model_column < value)
|
||||
return query.filter(self.column < value)
|
||||
|
||||
def filter_less_equal(self, query, value):
|
||||
"""
|
||||
|
@ -198,7 +212,7 @@ class AlchemyGridFilter(GridFilter):
|
|||
"""
|
||||
if value is None or value == '':
|
||||
return query
|
||||
return query.filter(self.model_column <= value)
|
||||
return query.filter(self.column <= value)
|
||||
|
||||
|
||||
class AlchemyStringFilter(AlchemyGridFilter):
|
||||
|
@ -219,7 +233,7 @@ class AlchemyStringFilter(AlchemyGridFilter):
|
|||
"""
|
||||
if value is None or value == '':
|
||||
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):
|
||||
"""
|
||||
|
@ -231,8 +245,8 @@ class AlchemyStringFilter(AlchemyGridFilter):
|
|||
# When saying something is 'not like' something else, we must also
|
||||
# include things which are nothing at all, in our result set.
|
||||
return query.filter(sa.or_(
|
||||
self.model_column == None,
|
||||
~self.model_column.ilike('%{0}%'.format(value)),
|
||||
self.column == None,
|
||||
~self.column.ilike('%{0}%'.format(value)),
|
||||
))
|
||||
|
||||
|
||||
|
@ -249,6 +263,27 @@ class AlchemyNumericFilter(AlchemyGridFilter):
|
|||
'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):
|
||||
"""
|
||||
Collection class for :class:`GridFilter` instances.
|
||||
|
|
|
@ -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()}
|
11
tailbone/templates/users/edit.mako
Normal file
11
tailbone/templates/users/edit.mako
Normal 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()}
|
|
@ -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()}
|
11
tailbone/templates/users/view.mako
Normal file
11
tailbone/templates/users/view.mako
Normal 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()}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue