Refactor user and role views to use master3
This commit is contained in:
parent
789bdef190
commit
86cfc59d33
|
@ -81,12 +81,6 @@ div.error-messages div.ui-state-error {
|
||||||
margin: 0.5em 0 0 0;
|
margin: 0.5em 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.error {
|
|
||||||
color: #dd6666;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.error {
|
ul.error {
|
||||||
color: #dd6666;
|
color: #dd6666;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
|
@ -45,47 +45,54 @@ div.fieldset {
|
||||||
* Fieldsets
|
* Fieldsets
|
||||||
******************************/
|
******************************/
|
||||||
|
|
||||||
div.field-wrapper {
|
.field-wrapper {
|
||||||
clear: both;
|
clear: both;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 5px;
|
margin: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper.error {
|
.field-wrapper.error {
|
||||||
background-color: #ddcccc;
|
background-color: #ddcccc;
|
||||||
border: 2px solid #dd6666;
|
border: 2px solid #dd6666;
|
||||||
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper label {
|
.field-wrapper .field-row {
|
||||||
display: block;
|
display: table-row;
|
||||||
float: left;
|
}
|
||||||
|
|
||||||
|
.field-wrapper label {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: top;
|
||||||
width: 15em;
|
width: 15em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-top: 2px;
|
padding-top: 2px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-wrapper.error label {
|
.field-wrapper.error label {
|
||||||
color: Black;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper div.field-error {
|
.field-wrapper .field-error {
|
||||||
|
padding: 1em 0 0.5em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-wrapper .field-error .error-msg {
|
||||||
color: #dd6666;
|
color: #dd6666;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper div.field {
|
.field-wrapper .field {
|
||||||
display: block;
|
display: table-cell;
|
||||||
float: left;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper div.field input[type=text],
|
.field-wrapper .field input[type=text],
|
||||||
div.field-wrapper div.field input[type=password],
|
.field-wrapper .field input[type=password],
|
||||||
div.field-wrapper div.field select,
|
.field-wrapper .field select,
|
||||||
div.field-wrapper div.field textarea {
|
.field-wrapper .field textarea {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +101,7 @@ label input[type="radio"] {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field ul {
|
.field ul {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,34 +3,30 @@
|
||||||
* Permission Lists
|
* Permission Lists
|
||||||
******************************/
|
******************************/
|
||||||
|
|
||||||
div.field-wrapper.permissions {
|
.field-wrapper.permissions .field .group {
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.field-wrapper.permissions div.field div.group {
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper.permissions div.field div.group p {
|
.field-wrapper.permissions .field .group p {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper.permissions div.field label {
|
.field-wrapper.permissions .field label {
|
||||||
float: none;
|
display: inline;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper.permissions div.field label input {
|
.field-wrapper.permissions .field label input {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper.permissions div.field div.group p.perm {
|
.field-wrapper.permissions .field .group p.perm {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.field-wrapper.permissions div.field div.group p.perm span {
|
.field-wrapper.permissions .field .group p.perm span {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
28
tailbone/templates/deform/permissions.pt
Normal file
28
tailbone/templates/deform/permissions.pt
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<div tal:define="oid oid|field.oid;
|
||||||
|
true_val true_val|field.widget.true_val;"
|
||||||
|
tal:omit-tag="">
|
||||||
|
${field.start_mapping()}
|
||||||
|
|
||||||
|
<tal:loop tal:repeat="groupkey sorted(permissions, key=lambda k: permissions[k]['label'].lower())">
|
||||||
|
<div tal:define="perms permissions[groupkey]['perms'];"
|
||||||
|
class="group">
|
||||||
|
<p class="group">${permissions[groupkey]['label']}</p>
|
||||||
|
|
||||||
|
<tal:loop tal:repeat="key sorted(perms, key=lambda p: perms[p]['label'].lower())">
|
||||||
|
<div class="perm">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox"
|
||||||
|
name="${key}"
|
||||||
|
id="${oid}-${key}"
|
||||||
|
value="${true_val}"
|
||||||
|
tal:attributes="checked python:field.widget.get_checked_value(cstruct, key);" />
|
||||||
|
${perms[key]['label']}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</tal:loop>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</tal:loop>
|
||||||
|
|
||||||
|
${field.end_mapping()}
|
||||||
|
</div>
|
|
@ -26,11 +26,13 @@ ${h.csrf_token(request)}
|
||||||
## <div class="field-error">${error}</div>
|
## <div class="field-error">${error}</div>
|
||||||
## % endfor
|
## % endfor
|
||||||
% if field.error:
|
% if field.error:
|
||||||
<div class="field-error">${field.error.msg}</div>
|
<div class="field-error"><span class="error-msg">${field.error.msg}</span></div>
|
||||||
% endif
|
% endif
|
||||||
<label for="${field.oid}">${field.title}</label>
|
<div class="field-row">
|
||||||
<div class="field">
|
<label for="${field.oid}">${field.title}</label>
|
||||||
${field.serialize()|n}
|
<div class="field">
|
||||||
|
${field.serialize()|n}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
% if form.has_helptext(field.name):
|
% if form.has_helptext(field.name):
|
||||||
<span class="instructions">${form.render_helptext(field.name)}</span>
|
<span class="instructions">${form.render_helptext(field.name)}</span>
|
||||||
|
|
|
@ -28,9 +28,14 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
import wtforms
|
from rattail.db.auth import has_permission
|
||||||
|
from rattail.core import Object
|
||||||
|
|
||||||
from tailbone.views import MasterView2 as MasterView
|
import wtforms
|
||||||
|
from webhelpers2.html import HTML
|
||||||
|
|
||||||
|
from tailbone.db import Session
|
||||||
|
from tailbone.views import MasterView3 as MasterView
|
||||||
|
|
||||||
|
|
||||||
class PrincipalMasterView(MasterView):
|
class PrincipalMasterView(MasterView):
|
||||||
|
@ -95,3 +100,33 @@ class PrincipalMasterView(MasterView):
|
||||||
permission='{}.find_by_perm'.format(permission_prefix))
|
permission='{}.find_by_perm'.format(permission_prefix))
|
||||||
config.add_tailbone_permission(permission_prefix, '{}.find_by_perm'.format(permission_prefix),
|
config.add_tailbone_permission(permission_prefix, '{}.find_by_perm'.format(permission_prefix),
|
||||||
"Find all {} with permission X".format(model_title_plural))
|
"Find all {} with permission X".format(model_title_plural))
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionsRenderer(Object):
|
||||||
|
permissions = None
|
||||||
|
include_guest = False
|
||||||
|
include_authenticated = False
|
||||||
|
|
||||||
|
def __call__(self, principal, field):
|
||||||
|
self.principal = principal
|
||||||
|
return self.render()
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
principal = self.principal
|
||||||
|
html = ''
|
||||||
|
for groupkey in sorted(self.permissions, key=lambda k: self.permissions[k]['label'].lower()):
|
||||||
|
inner = HTML.tag('p', c=self.permissions[groupkey]['label'])
|
||||||
|
perms = self.permissions[groupkey]['perms']
|
||||||
|
rendered = False
|
||||||
|
for key in sorted(perms, key=lambda p: perms[p]['label'].lower()):
|
||||||
|
checked = has_permission(Session(), principal, key,
|
||||||
|
include_guest=self.include_guest,
|
||||||
|
include_authenticated=self.include_authenticated)
|
||||||
|
if checked:
|
||||||
|
label = perms[key]['label']
|
||||||
|
span = HTML.tag('span', c="[X]" if checked else "[ ]")
|
||||||
|
inner += HTML.tag('p', class_='perm', c=span + ' ' + label)
|
||||||
|
rendered = True
|
||||||
|
if rendered:
|
||||||
|
html += HTML.tag('div', class_='group', c=inner)
|
||||||
|
return html or "(none granted)"
|
||||||
|
|
|
@ -26,17 +26,18 @@ Role Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import six
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
from rattail.db.auth import has_permission, administrator_role, guest_role, authenticated_role
|
from rattail.db.auth import has_permission, administrator_role, guest_role, authenticated_role
|
||||||
|
|
||||||
import formalchemy as fa
|
import colander
|
||||||
from formalchemy.fields import IntegerFieldRenderer
|
from deform import widget as dfwidget
|
||||||
|
|
||||||
from tailbone import forms, grids
|
from tailbone import grids
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views.principal import PrincipalMasterView
|
from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer
|
||||||
|
|
||||||
|
|
||||||
class RolesView(PrincipalMasterView):
|
class RolesView(PrincipalMasterView):
|
||||||
|
@ -51,6 +52,12 @@ class RolesView(PrincipalMasterView):
|
||||||
'session_timeout',
|
'session_timeout',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
'name',
|
||||||
|
'session_timeout',
|
||||||
|
'permissions',
|
||||||
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super(RolesView, self).configure_grid(g)
|
super(RolesView, self).configure_grid(g)
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
|
@ -58,21 +65,39 @@ class RolesView(PrincipalMasterView):
|
||||||
g.set_sort_defaults('name')
|
g.set_sort_defaults('name')
|
||||||
g.set_link('name')
|
g.set_link('name')
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def configure_form(self, f):
|
||||||
fs.append(PermissionsField('permissions'))
|
super(RolesView, self).configure_form(f)
|
||||||
permissions = self.request.registry.settings.get('tailbone_permissions', {})
|
role = f.model_instance
|
||||||
fs.permissions.set(renderer=forms.renderers.PermissionsFieldRenderer(permissions))
|
|
||||||
fs.session_timeout.set(renderer=SessionTimeoutRenderer)
|
|
||||||
if (self.viewing or self.editing) and fs.model is guest_role(self.Session()):
|
|
||||||
fs.session_timeout.set(readonly=True, attrs={'applicable': False})
|
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
# permissions
|
||||||
fs.configure(
|
self.tailbone_permissions = self.request.registry.settings.get('tailbone_permissions', {})
|
||||||
include=[
|
f.set_renderer('permissions', PermissionsRenderer(permissions=self.tailbone_permissions))
|
||||||
fs.name,
|
f.set_node('permissions', colander.Set())
|
||||||
fs.session_timeout,
|
f.set_widget('permissions', PermissionsWidget(permissions=self.tailbone_permissions))
|
||||||
fs.permissions,
|
if self.editing:
|
||||||
])
|
granted = []
|
||||||
|
for groupkey in self.tailbone_permissions:
|
||||||
|
for key in self.tailbone_permissions[groupkey]['perms']:
|
||||||
|
if has_permission(self.Session(), role, key, include_guest=False, include_authenticated=False):
|
||||||
|
granted.append(key)
|
||||||
|
f.set_default('permissions', granted)
|
||||||
|
|
||||||
|
# session_timeout
|
||||||
|
f.set_renderer('session_timeout', self.render_session_timeout)
|
||||||
|
if self.editing and role is guest_role(self.Session()):
|
||||||
|
f.set_readonly('session_timeout')
|
||||||
|
|
||||||
|
def render_session_timeout(self, role, field):
|
||||||
|
if role is guest_role(self.Session()):
|
||||||
|
return "(not applicable)"
|
||||||
|
if role.session_timeout is None:
|
||||||
|
return ""
|
||||||
|
return six.text_type(role.session_timeout)
|
||||||
|
|
||||||
|
def objectify(self, form, data):
|
||||||
|
role = super(RolesView, self).objectify(form, data)
|
||||||
|
role.permissions = data['permissions']
|
||||||
|
return role
|
||||||
|
|
||||||
def template_kwargs_view(self, **kwargs):
|
def template_kwargs_view(self, **kwargs):
|
||||||
role = kwargs['instance']
|
role = kwargs['instance']
|
||||||
|
@ -86,14 +111,14 @@ class RolesView(PrincipalMasterView):
|
||||||
main_actions=actions)
|
main_actions=actions)
|
||||||
else:
|
else:
|
||||||
kwargs['users'] = None
|
kwargs['users'] = None
|
||||||
kwargs['guest_role'] = guest_role(Session())
|
kwargs['guest_role'] = guest_role(self.Session())
|
||||||
kwargs['authenticated_role'] = authenticated_role(Session())
|
kwargs['authenticated_role'] = authenticated_role(self.Session())
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def before_delete(self, role):
|
def before_delete(self, role):
|
||||||
admin = administrator_role(Session())
|
admin = administrator_role(self.Session())
|
||||||
guest = guest_role(Session())
|
guest = guest_role(self.Session())
|
||||||
authenticated = authenticated_role(Session())
|
authenticated = authenticated_role(self.Session())
|
||||||
if role in (admin, guest, authenticated):
|
if role in (admin, guest, authenticated):
|
||||||
self.request.session.flash("You may not delete the {} role.".format(role.name), 'error')
|
self.request.session.flash("You may not delete the {} role.".format(role.name), 'error')
|
||||||
return self.redirect(self.request.get_referrer(default=self.request.route_url('roles')))
|
return self.redirect(self.request.get_referrer(default=self.request.route_url('roles')))
|
||||||
|
@ -110,23 +135,25 @@ class RolesView(PrincipalMasterView):
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
|
|
||||||
class SessionTimeoutRenderer(IntegerFieldRenderer):
|
class PermissionsWidget(dfwidget.Widget):
|
||||||
|
template = 'permissions'
|
||||||
|
permissions = None
|
||||||
|
true_val = 'true'
|
||||||
|
|
||||||
def render_readonly(self, **kwargs):
|
def deserialize(self, field, pstruct):
|
||||||
if not kwargs.pop('applicable', True):
|
return [key for key, val in pstruct.items()
|
||||||
return "(not applicable)"
|
if val == self.true_val]
|
||||||
return super(SessionTimeoutRenderer, self).render_readonly(**kwargs)
|
|
||||||
|
|
||||||
|
def get_checked_value(self, cstruct, value):
|
||||||
|
if cstruct is colander.null:
|
||||||
|
return False
|
||||||
|
return value in cstruct
|
||||||
|
|
||||||
class PermissionsField(fa.Field):
|
def serialize(self, field, cstruct, **kw):
|
||||||
"""
|
kw.setdefault('permissions', self.permissions)
|
||||||
Custom field for role permissions.
|
template = self.template
|
||||||
"""
|
values = self.get_template_values(field, cstruct, kw)
|
||||||
|
return field.renderer(template, **values)
|
||||||
def sync(self):
|
|
||||||
if not self.is_readonly():
|
|
||||||
role = self.model
|
|
||||||
role.permissions = self.renderer.deserialize()
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
|
@ -28,102 +28,22 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
|
import six
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
from rattail.db.auth import guest_role, authenticated_role, set_user_password, has_permission
|
from rattail.db.auth import guest_role, authenticated_role, set_user_password, has_permission
|
||||||
|
|
||||||
import wtforms
|
import colander
|
||||||
import formalchemy
|
from deform import widget as dfwidget
|
||||||
from formalchemy.fields import SelectFieldRenderer
|
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from tailbone import forms
|
from tailbone import forms2 as forms
|
||||||
from tailbone.db import Session
|
from tailbone.db import Session
|
||||||
from tailbone.views import MasterView2 as MasterView
|
from tailbone.views import MasterView3 as MasterView
|
||||||
from tailbone.views.principal import PrincipalMasterView
|
from tailbone.views.principal import PrincipalMasterView, PermissionsRenderer
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
if field.parent.confirm_password.value != value:
|
|
||||||
raise formalchemy.ValidationError("Passwords do not match")
|
|
||||||
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):
|
|
||||||
kwargs.setdefault('value', lambda x: x.password)
|
|
||||||
kwargs.setdefault('renderer', PasswordFieldRenderer)
|
|
||||||
kwargs.setdefault('validate', passwords_match)
|
|
||||||
super(PasswordField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def sync(self):
|
|
||||||
if not self.is_readonly():
|
|
||||||
password = self.renderer.deserialize()
|
|
||||||
if password:
|
|
||||||
set_user_password(self.model, password)
|
|
||||||
|
|
||||||
|
|
||||||
def RolesFieldRenderer(request):
|
|
||||||
|
|
||||||
class RolesFieldRenderer(SelectFieldRenderer):
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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)\
|
|
||||||
.filter(model.Role.uuid != authenticated_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(PrincipalMasterView):
|
class UsersView(PrincipalMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for the User model.
|
Master view for the User model.
|
||||||
|
@ -133,6 +53,29 @@ class UsersView(PrincipalMasterView):
|
||||||
model_row_class = model.UserEvent
|
model_row_class = model.UserEvent
|
||||||
has_versions = True
|
has_versions = True
|
||||||
|
|
||||||
|
grid_columns = [
|
||||||
|
'username',
|
||||||
|
'person',
|
||||||
|
'active',
|
||||||
|
]
|
||||||
|
|
||||||
|
form_fields = [
|
||||||
|
'username',
|
||||||
|
'person',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
'display_name',
|
||||||
|
'active',
|
||||||
|
'active_sticky',
|
||||||
|
'password',
|
||||||
|
'roles',
|
||||||
|
]
|
||||||
|
|
||||||
|
row_grid_columns = [
|
||||||
|
'type_code',
|
||||||
|
'occurred',
|
||||||
|
]
|
||||||
|
|
||||||
mergeable = True
|
mergeable = True
|
||||||
merge_additive_fields = [
|
merge_additive_fields = [
|
||||||
'sent_message_count',
|
'sent_message_count',
|
||||||
|
@ -147,17 +90,6 @@ class UsersView(PrincipalMasterView):
|
||||||
'active',
|
'active',
|
||||||
]
|
]
|
||||||
|
|
||||||
grid_columns = [
|
|
||||||
'username',
|
|
||||||
'person',
|
|
||||||
'active',
|
|
||||||
]
|
|
||||||
|
|
||||||
row_grid_columns = [
|
|
||||||
'type_code',
|
|
||||||
'occurred',
|
|
||||||
]
|
|
||||||
|
|
||||||
def query(self, session):
|
def query(self, session):
|
||||||
return session.query(model.User)\
|
return session.query(model.User)\
|
||||||
.outerjoin(model.Person)\
|
.outerjoin(model.Person)\
|
||||||
|
@ -191,45 +123,119 @@ class UsersView(PrincipalMasterView):
|
||||||
g.set_link('last_name')
|
g.set_link('last_name')
|
||||||
g.set_link('display_name')
|
g.set_link('display_name')
|
||||||
|
|
||||||
def _preconfigure_fieldset(self, fs):
|
def unique_username(self, node, value):
|
||||||
fs.username.set(renderer=forms.renderers.StrippedTextFieldRenderer, validate=unique_username)
|
query = self.Session.query(model.User)\
|
||||||
fs.person.set(renderer=forms.renderers.PersonFieldRenderer, options=[])
|
.filter(model.User.username == value)
|
||||||
fs.append(PasswordField('password', label="Set Password"))
|
if self.editing:
|
||||||
fs.append(formalchemy.Field('confirm_password', renderer=PasswordFieldRenderer))
|
user = self.get_instance()
|
||||||
fs.append(RolesField('roles', renderer=RolesFieldRenderer(self.request), size=10))
|
query = query.filter(model.User.uuid != user.uuid)
|
||||||
fs.append(forms.AssociationProxyField('first_name'))
|
if query.count():
|
||||||
fs.append(forms.AssociationProxyField('last_name'))
|
raise colander.Invalid(node, "Username must be unique")
|
||||||
fs.append(forms.AssociationProxyField('display_name', label="Full Name"))
|
|
||||||
|
|
||||||
# hm this should work according to MDN but doesn't seem to...
|
def configure_form(self, f):
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
super(UsersView, self).configure_form(f)
|
||||||
fs.username.attrs(autocomplete='new-password')
|
user = f.model_instance
|
||||||
fs.password.attrs(autocomplete='new-password')
|
|
||||||
fs.confirm_password.attrs(autocomplete='new-password')
|
# username
|
||||||
|
f.set_validator('username', self.unique_username)
|
||||||
|
|
||||||
|
# person
|
||||||
|
f.set_renderer('person', self.render_person)
|
||||||
|
if self.creating or self.editing:
|
||||||
|
f.replace('person', 'person_uuid')
|
||||||
|
f.set_node('person_uuid', colander.String(), missing=colander.null)
|
||||||
|
person_display = ""
|
||||||
|
if self.request.method == 'POST':
|
||||||
|
if self.request.POST.get('person_uuid'):
|
||||||
|
person = self.Session.query(model.Person).get(self.request.POST['person_uuid'])
|
||||||
|
if person:
|
||||||
|
person_display = six.text_type(person)
|
||||||
|
elif self.editing:
|
||||||
|
person_display = six.text_type(user.person or '')
|
||||||
|
people_url = self.request.route_url('people.autocomplete')
|
||||||
|
f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget(
|
||||||
|
field_display=person_display, service_url=people_url))
|
||||||
|
f.set_label('person_uuid', "Person")
|
||||||
|
|
||||||
|
# password
|
||||||
|
f.set_widget('password', dfwidget.CheckedPasswordWidget())
|
||||||
|
f.set_label('password', "Set Password")
|
||||||
|
# if self.creating:
|
||||||
|
# f.set_required('password')
|
||||||
|
|
||||||
|
# roles
|
||||||
|
f.set_renderer('roles', self.render_roles)
|
||||||
|
if self.creating or self.editing:
|
||||||
|
roles = self.get_possible_roles().all()
|
||||||
|
role_values = [(s.uuid, six.text_type(s)) for s in roles]
|
||||||
|
f.set_node('roles', colander.Set())
|
||||||
|
f.set_widget('roles', dfwidget.SelectWidget(multiple=True,
|
||||||
|
size=len(roles),
|
||||||
|
values=role_values))
|
||||||
|
if self.editing:
|
||||||
|
f.set_default('roles', [r.uuid for r in user.roles])
|
||||||
|
|
||||||
|
f.set_label('display_name', "Full Name")
|
||||||
|
|
||||||
|
# # hm this should work according to MDN but doesn't seem to...
|
||||||
|
# # https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
|
||||||
|
# fs.username.attrs(autocomplete='new-password')
|
||||||
|
# fs.password.attrs(autocomplete='new-password')
|
||||||
|
# fs.confirm_password.attrs(autocomplete='new-password')
|
||||||
|
|
||||||
def configure_fieldset(self, fs):
|
|
||||||
fs.configure(
|
|
||||||
include=[
|
|
||||||
fs.username,
|
|
||||||
fs.person,
|
|
||||||
fs.first_name,
|
|
||||||
fs.last_name,
|
|
||||||
fs.display_name,
|
|
||||||
fs.active,
|
|
||||||
fs.active_sticky,
|
|
||||||
fs.password,
|
|
||||||
fs.confirm_password,
|
|
||||||
fs.roles,
|
|
||||||
])
|
|
||||||
if self.viewing:
|
if self.viewing:
|
||||||
permissions = self.request.registry.settings.get('tailbone_permissions', {})
|
permissions = self.request.registry.settings.get('tailbone_permissions', {})
|
||||||
renderer = forms.renderers.PermissionsFieldRenderer(permissions,
|
f.append('permissions')
|
||||||
include_guest=True,
|
f.set_renderer('permissions', PermissionsRenderer(permissions=permissions,
|
||||||
include_authenticated=True)
|
include_guest=True,
|
||||||
fs.append(formalchemy.Field('permissions', renderer=renderer))
|
include_authenticated=True))
|
||||||
|
|
||||||
if self.viewing or self.deleting:
|
if self.viewing or self.deleting:
|
||||||
del fs.password
|
f.remove('password')
|
||||||
del fs.confirm_password
|
|
||||||
|
def get_possible_roles(self):
|
||||||
|
excluded = [
|
||||||
|
guest_role(self.Session()).uuid,
|
||||||
|
authenticated_role(self.Session()).uuid,
|
||||||
|
]
|
||||||
|
return self.Session.query(model.Role)\
|
||||||
|
.filter(~model.Role.uuid.in_(excluded))\
|
||||||
|
.order_by(model.Role.name)
|
||||||
|
|
||||||
|
def objectify(self, form, data):
|
||||||
|
user = super(UsersView, self).objectify(form, data)
|
||||||
|
if data['password']:
|
||||||
|
set_user_password(user, data['password'])
|
||||||
|
self.update_roles(user, data)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def update_roles(self, user, data):
|
||||||
|
old_roles = set([r.uuid for r in user.roles])
|
||||||
|
new_roles = data['roles']
|
||||||
|
for uuid in new_roles:
|
||||||
|
if uuid not in old_roles:
|
||||||
|
user._roles.append(model.UserRole(role_uuid=uuid))
|
||||||
|
for uuid in old_roles:
|
||||||
|
if uuid not in new_roles:
|
||||||
|
role = self.Session.query(model.Role).get(uuid)
|
||||||
|
user.roles.remove(role)
|
||||||
|
|
||||||
|
def render_person(self, user, field):
|
||||||
|
person = user.person
|
||||||
|
if not person:
|
||||||
|
return ""
|
||||||
|
text = six.text_type(person)
|
||||||
|
url = self.request.route_url('people.view', uuid=person.uuid)
|
||||||
|
return tags.link_to(person, url)
|
||||||
|
|
||||||
|
def render_roles(self, user, field):
|
||||||
|
roles = user.roles
|
||||||
|
items = []
|
||||||
|
for role in roles:
|
||||||
|
text = role.name
|
||||||
|
url = self.request.route_url('roles.view', uuid=role.uuid)
|
||||||
|
items.append(HTML.tag('li', c=tags.link_to(text, url)))
|
||||||
|
return HTML.tag('ul', c=items)
|
||||||
|
|
||||||
def editable_instance(self, user):
|
def editable_instance(self, user):
|
||||||
if self.rattail_config.demo():
|
if self.rattail_config.demo():
|
||||||
|
|
Loading…
Reference in a new issue