From af07f477dcd069049b1250da4702e5f0aa79936a Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 11 Aug 2015 23:19:41 -0500 Subject: [PATCH] Convert User pages to use master view. And of course make some more tweaks to new grids etc. --- tailbone/app.py | 3 + tailbone/newgrids/alchemy.py | 52 ++---- tailbone/newgrids/core.py | 26 ++- tailbone/newgrids/filters.py | 69 +++++-- tailbone/templates/users/crud.mako | 16 -- tailbone/templates/users/edit.mako | 11 ++ tailbone/templates/users/index.mako | 12 -- tailbone/templates/users/view.mako | 11 ++ tailbone/views/master.py | 84 +++++---- tailbone/views/roles.py | 2 +- tailbone/views/users.py | 272 +++++++++++----------------- 11 files changed, 269 insertions(+), 289 deletions(-) delete mode 100644 tailbone/templates/users/crud.mako create mode 100644 tailbone/templates/users/edit.mako delete mode 100644 tailbone/templates/users/index.mako create mode 100644 tailbone/templates/users/view.mako diff --git a/tailbone/app.py b/tailbone/app.py index 43db4c6e..0fffdd68 100644 --- a/tailbone/app.py +++ b/tailbone/app.py @@ -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 diff --git a/tailbone/newgrids/alchemy.py b/tailbone/newgrids/alchemy.py index b7a2a40b..8719359f 100644 --- a/tailbone/newgrids/alchemy.py +++ b/tailbone/newgrids/alchemy.py @@ -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 diff --git a/tailbone/newgrids/core.py b/tailbone/newgrids/core.py index 7f0a9712..0b1397c3 100644 --- a/tailbone/newgrids/core.py +++ b/tailbone/newgrids/core.py @@ -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,15 +379,31 @@ 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. """ - 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): """ @@ -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) diff --git a/tailbone/newgrids/filters.py b/tailbone/newgrids/filters.py index 16c58099..56bcd70b 100644 --- a/tailbone/newgrids/filters.py +++ b/tailbone/newgrids/filters.py @@ -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. diff --git a/tailbone/templates/users/crud.mako b/tailbone/templates/users/crud.mako deleted file mode 100644 index b46ca1ed..00000000 --- a/tailbone/templates/users/crud.mako +++ /dev/null @@ -1,16 +0,0 @@ -## -*- coding: utf-8 -*- -<%inherit file="/crud.mako" /> - -<%def name="context_menu_items()"> -
  • ${h.link_to("Back to Users", url('users'))}
  • - % if form.readonly: -
  • ${h.link_to("Edit this User", url('user.update', uuid=form.fieldset.model.uuid))}
  • - % elif form.updating: -
  • ${h.link_to("View this User", url('user.read', uuid=form.fieldset.model.uuid))}
  • - % endif - % if version_count is not Undefined and request.has_perm('user.versions.view'): -
  • ${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=form.fieldset.model.uuid))}
  • - % endif - - -${parent.body()} diff --git a/tailbone/templates/users/edit.mako b/tailbone/templates/users/edit.mako new file mode 100644 index 00000000..3e7334c5 --- /dev/null +++ b/tailbone/templates/users/edit.mako @@ -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'): +
  • ${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=instance.uuid))}
  • + % endif + + +${parent.body()} diff --git a/tailbone/templates/users/index.mako b/tailbone/templates/users/index.mako deleted file mode 100644 index a7ec4532..00000000 --- a/tailbone/templates/users/index.mako +++ /dev/null @@ -1,12 +0,0 @@ -## -*- coding: utf-8 -*- -<%inherit file="/grid.mako" /> - -<%def name="title()">Users - -<%def name="context_menu_items()"> - % if request.has_perm('users.create'): -
  • ${h.link_to("Create a new User", url('user.create'))}
  • - % endif - - -${parent.body()} diff --git a/tailbone/templates/users/view.mako b/tailbone/templates/users/view.mako new file mode 100644 index 00000000..6dbc4c8a --- /dev/null +++ b/tailbone/templates/users/view.mako @@ -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'): +
  • ${h.link_to("View Change History ({0})".format(version_count), url('user.versions', uuid=instance.uuid))}
  • + % endif + + +${parent.body()} diff --git a/tailbone/views/master.py b/tailbone/views/master.py index 57aa647b..ef26c618 100644 --- a/tailbone/views/master.py +++ b/tailbone/views/master.py @@ -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 diff --git a/tailbone/views/roles.py b/tailbone/views/roles.py index f2d40c5b..858775e4 100644 --- a/tailbone/views/roles.py +++ b/tailbone/views/roles.py @@ -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 diff --git a/tailbone/views/users.py b/tailbone/views/users.py index e15f5462..df9ba9f8 100644 --- a/tailbone/views/users.py +++ b/tailbone/views/users.py @@ -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')