feat: expose User "roles" for editing
This commit is contained in:
parent
bdfa0197b2
commit
97e914c2e0
13 changed files with 492 additions and 39 deletions
|
@ -120,6 +120,13 @@ class Form:
|
|||
|
||||
See also :meth:`set_validator()`.
|
||||
|
||||
.. attribute:: defaults
|
||||
|
||||
Dict of default field values, used to construct the form in
|
||||
:meth:`get_schema()`.
|
||||
|
||||
See also :meth:`set_default()`.
|
||||
|
||||
.. attribute:: readonly
|
||||
|
||||
Boolean indicating the form does not allow submit. In practice
|
||||
|
@ -248,6 +255,7 @@ class Form:
|
|||
nodes={},
|
||||
widgets={},
|
||||
validators={},
|
||||
defaults={},
|
||||
readonly=False,
|
||||
readonly_fields=[],
|
||||
required_fields={},
|
||||
|
@ -271,6 +279,7 @@ class Form:
|
|||
self.nodes = nodes or {}
|
||||
self.widgets = widgets or {}
|
||||
self.validators = validators or {}
|
||||
self.defaults = defaults or {}
|
||||
self.readonly = readonly
|
||||
self.readonly_fields = set(readonly_fields or [])
|
||||
self.required_fields = required_fields or {}
|
||||
|
@ -375,6 +384,23 @@ class Form:
|
|||
"""
|
||||
self.fields = FieldList(fields)
|
||||
|
||||
def append(self, *keys):
|
||||
"""
|
||||
Add some fields(s) to the form.
|
||||
|
||||
This is a convenience to allow adding multiple fields at
|
||||
once::
|
||||
|
||||
form.append('first_field',
|
||||
'second_field',
|
||||
'third_field')
|
||||
|
||||
It will add each field to :attr:`fields`.
|
||||
"""
|
||||
for key in keys:
|
||||
if key not in self.fields:
|
||||
self.fields.append(key)
|
||||
|
||||
def remove(self, *keys):
|
||||
"""
|
||||
Remove some fields(s) from the form.
|
||||
|
@ -471,6 +497,18 @@ class Form:
|
|||
if self.schema and key in self.schema:
|
||||
self.schema[key].validator = validator
|
||||
|
||||
def set_default(self, key, value):
|
||||
"""
|
||||
Set/override the default value for a field.
|
||||
|
||||
:param key: Name of field.
|
||||
|
||||
:param validator: Default value for the field.
|
||||
|
||||
Default value overrides are tracked via :attr:`defaults`.
|
||||
"""
|
||||
self.defaults[key] = value
|
||||
|
||||
def set_readonly(self, key, readonly=True):
|
||||
"""
|
||||
Enable or disable the "readonly" flag for a given field.
|
||||
|
@ -624,30 +662,17 @@ class Form:
|
|||
|
||||
if self.model_class:
|
||||
|
||||
# first define full list of 'includes' - final schema
|
||||
# should contain all of these fields
|
||||
includes = list(fields)
|
||||
|
||||
# determine which we want ColanderAlchemy to handle
|
||||
auto_includes = []
|
||||
for key in includes:
|
||||
|
||||
# skip if we already have a node defined
|
||||
# collect list of field names and/or nodes
|
||||
includes = []
|
||||
for key in fields:
|
||||
if key in self.nodes:
|
||||
continue
|
||||
|
||||
# we want the magic for this field
|
||||
auto_includes.append(key)
|
||||
includes.append(self.nodes[key])
|
||||
else:
|
||||
includes.append(key)
|
||||
|
||||
# make initial schema with ColanderAlchemy magic
|
||||
schema = SQLAlchemySchemaNode(self.model_class,
|
||||
includes=auto_includes)
|
||||
|
||||
# now fill in the blanks for non-magic fields
|
||||
for key in includes:
|
||||
if key not in auto_includes:
|
||||
node = self.nodes[key]
|
||||
schema.add(node)
|
||||
includes=includes)
|
||||
|
||||
else:
|
||||
|
||||
|
@ -685,6 +710,11 @@ class Form:
|
|||
elif key in schema: # field-level
|
||||
schema[key].validator = validator
|
||||
|
||||
# apply default value overrides
|
||||
for key, value in self.defaults.items():
|
||||
if key in schema:
|
||||
schema[key].default = value
|
||||
|
||||
# apply required flags
|
||||
for key, required in self.required_fields.items():
|
||||
if key in schema:
|
||||
|
|
|
@ -257,3 +257,44 @@ class PersonRef(ObjectRef):
|
|||
def sort_query(self, query):
|
||||
""" """
|
||||
return query.order_by(self.model_class.full_name)
|
||||
|
||||
|
||||
class RoleRefs(colander.Set):
|
||||
"""
|
||||
Form schema type for the User
|
||||
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles`
|
||||
association proxy field.
|
||||
"""
|
||||
|
||||
def __init__(self, request, session=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
self.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
self.session = session or Session()
|
||||
|
||||
def widget_maker(self, **kwargs):
|
||||
"""
|
||||
Constructs a default widget for the field.
|
||||
|
||||
:returns: Instance of
|
||||
:class:`~wuttaweb.forms.widgets.RoleRefsWidget`.
|
||||
"""
|
||||
kwargs.setdefault('session', self.session)
|
||||
|
||||
if 'values' not in kwargs:
|
||||
model = self.app.model
|
||||
auth = self.app.get_auth_handler()
|
||||
avoid = {
|
||||
auth.get_role_authenticated(self.session),
|
||||
auth.get_role_anonymous(self.session),
|
||||
}
|
||||
avoid = set([role.uuid for role in avoid])
|
||||
roles = self.session.query(model.Role)\
|
||||
.filter(~model.Role.uuid.in_(avoid))\
|
||||
.order_by(model.Role.name)\
|
||||
.all()
|
||||
values = [(role.uuid, role.name) for role in roles]
|
||||
kwargs['values'] = values
|
||||
|
||||
return widgets.RoleRefsWidget(self.request, **kwargs)
|
||||
|
|
|
@ -32,11 +32,16 @@ in the namespace:
|
|||
* :class:`deform:deform.widget.TextInputWidget`
|
||||
* :class:`deform:deform.widget.TextAreaWidget`
|
||||
* :class:`deform:deform.widget.SelectWidget`
|
||||
* :class:`deform:deform.widget.CheckboxChoiceWidget`
|
||||
"""
|
||||
|
||||
from deform.widget import Widget, TextInputWidget, TextAreaWidget, SelectWidget
|
||||
import colander
|
||||
from deform.widget import (Widget, TextInputWidget, TextAreaWidget,
|
||||
SelectWidget, CheckboxChoiceWidget)
|
||||
from webhelpers2.html import HTML
|
||||
|
||||
from wuttaweb.db import Session
|
||||
|
||||
|
||||
class ObjectRefWidget(SelectWidget):
|
||||
"""
|
||||
|
@ -96,3 +101,45 @@ class NotesWidget(TextAreaWidget):
|
|||
* ``readonly/notes``
|
||||
"""
|
||||
readonly_template = 'readonly/notes'
|
||||
|
||||
|
||||
class RoleRefsWidget(CheckboxChoiceWidget):
|
||||
"""
|
||||
Widget for use with User
|
||||
:attr:`~wuttjamaican:wuttjamaican.db.model.auth.User.roles` field.
|
||||
|
||||
This is a subclass of
|
||||
:class:`deform:deform.widget.CheckboxChoiceWidget` and uses these
|
||||
Deform templates:
|
||||
|
||||
* ``checkbox_choice``
|
||||
* ``readonly/checkbox_choice``
|
||||
"""
|
||||
|
||||
def __init__(self, request, session=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.request = request
|
||||
self.config = self.request.wutta_config
|
||||
self.app = self.config.get_app()
|
||||
self.session = session or Session()
|
||||
|
||||
def serialize(self, field, cstruct, **kw):
|
||||
""" """
|
||||
# special logic when field is editable
|
||||
readonly = kw.get('readonly', self.readonly)
|
||||
if not readonly:
|
||||
|
||||
# but does not apply if current user is root
|
||||
if not self.request.is_root:
|
||||
auth = self.app.get_auth_handler()
|
||||
admin = auth.get_role_administrator(self.session)
|
||||
|
||||
# prune admin role from values list; it should not be
|
||||
# one of the options since current user is not admin
|
||||
values = kw.get('values', self.values)
|
||||
values = [val for val in values
|
||||
if val[0] != admin.uuid]
|
||||
kw['values'] = values
|
||||
|
||||
# default logic from here
|
||||
return super().serialize(field, cstruct, **kw)
|
||||
|
|
|
@ -186,6 +186,23 @@ class Grid:
|
|||
"""
|
||||
self.columns = FieldList(columns)
|
||||
|
||||
def append(self, *keys):
|
||||
"""
|
||||
Add some columns(s) to the grid.
|
||||
|
||||
This is a convenience to allow adding multiple columns at
|
||||
once::
|
||||
|
||||
grid.append('first_field',
|
||||
'second_field',
|
||||
'third_field')
|
||||
|
||||
It will add each column to :attr:`columns`.
|
||||
"""
|
||||
for key in keys:
|
||||
if key not in self.columns:
|
||||
self.columns.append(key)
|
||||
|
||||
def remove(self, *keys):
|
||||
"""
|
||||
Remove some column(s) from the grid.
|
||||
|
|
18
src/wuttaweb/templates/deform/checkbox_choice.pt
Normal file
18
src/wuttaweb/templates/deform/checkbox_choice.pt
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div tal:define="css_class css_class|field.widget.css_class;
|
||||
style style|field.widget.style;
|
||||
oid oid|field.oid;">
|
||||
${field.start_sequence()}
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<div tal:repeat="choice values | field.widget.values"
|
||||
tal:omit-tag="">
|
||||
<b-checkbox tal:define="(value, title) choice"
|
||||
name="checkbox"
|
||||
native-value="${value}"
|
||||
tal:attributes=":value 'true' if value in cstruct else 'false';
|
||||
attributes|field.widget.attributes|{};">
|
||||
${title}
|
||||
</b-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
${field.end_sequence()}
|
||||
</div>
|
|
@ -28,6 +28,8 @@ import importlib
|
|||
import json
|
||||
import logging
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
import colander
|
||||
from webhelpers2.html import HTML, tags
|
||||
|
||||
|
@ -420,14 +422,17 @@ def get_model_fields(config, model_class=None):
|
|||
that to determine the field listing if applicable. Otherwise this
|
||||
returns ``None``.
|
||||
"""
|
||||
if model_class:
|
||||
import sqlalchemy as sa
|
||||
app = config.get_app()
|
||||
model = app.model
|
||||
if model_class and issubclass(model_class, model.Base):
|
||||
mapper = sa.inspect(model_class)
|
||||
fields = list([prop.key for prop in mapper.iterate_properties])
|
||||
return fields
|
||||
if not model_class:
|
||||
return
|
||||
|
||||
app = config.get_app()
|
||||
model = app.model
|
||||
if not issubclass(model_class, model.Base):
|
||||
return
|
||||
|
||||
mapper = sa.inspect(model_class)
|
||||
fields = [prop.key for prop in mapper.iterate_properties]
|
||||
return fields
|
||||
|
||||
|
||||
def make_json_safe(value, key=None, warn=True):
|
||||
|
|
|
@ -28,7 +28,7 @@ import colander
|
|||
|
||||
from wuttjamaican.db.model import User
|
||||
from wuttaweb.views import MasterView
|
||||
from wuttaweb.forms.schema import PersonRef
|
||||
from wuttaweb.forms.schema import PersonRef, RoleRefs
|
||||
from wuttaweb.db import Session
|
||||
|
||||
|
||||
|
@ -77,10 +77,10 @@ class UserView(MasterView):
|
|||
def configure_form(self, f):
|
||||
""" """
|
||||
super().configure_form(f)
|
||||
user = f.model_instance
|
||||
|
||||
# never show these
|
||||
f.remove('person_uuid',
|
||||
'password',
|
||||
'role_refs')
|
||||
|
||||
# person
|
||||
|
@ -90,6 +90,18 @@ class UserView(MasterView):
|
|||
# username
|
||||
f.set_validator('username', self.unique_username)
|
||||
|
||||
# password
|
||||
# if not (self.creating or self.editing):
|
||||
# f.remove('password')
|
||||
f.remove('password')
|
||||
|
||||
# roles
|
||||
f.append('roles')
|
||||
f.set_node('roles', RoleRefs(self.request))
|
||||
|
||||
if not self.creating:
|
||||
f.set_default('roles', [role.uuid for role in user.roles])
|
||||
|
||||
def unique_username(self, node, value):
|
||||
""" """
|
||||
model = self.app.model
|
||||
|
@ -105,6 +117,62 @@ class UserView(MasterView):
|
|||
if query.count():
|
||||
node.raise_invalid("Username must be unique")
|
||||
|
||||
def objectify(self, form, session=None):
|
||||
""" """
|
||||
# normal logic first
|
||||
user = super().objectify(form)
|
||||
|
||||
# update roles for user
|
||||
# TODO
|
||||
# if self.has_perm('edit_roles'):
|
||||
self.update_roles(user, form, session=session)
|
||||
|
||||
return user
|
||||
|
||||
def update_roles(self, user, form, session=None):
|
||||
""" """
|
||||
# TODO
|
||||
# if not self.has_perm('edit_roles'):
|
||||
# return
|
||||
data = form.validated
|
||||
if 'roles' not in data:
|
||||
return
|
||||
|
||||
model = self.app.model
|
||||
session = session or Session()
|
||||
auth = self.app.get_auth_handler()
|
||||
|
||||
old_roles = set([role.uuid for role in user.roles])
|
||||
new_roles = data['roles']
|
||||
|
||||
admin = auth.get_role_administrator(session)
|
||||
ignored = {
|
||||
auth.get_role_authenticated(session).uuid,
|
||||
auth.get_role_anonymous(session).uuid,
|
||||
}
|
||||
|
||||
# add any new roles for the user, taking care to avoid certain
|
||||
# unwanted operations for built-in roles
|
||||
for uuid in new_roles:
|
||||
if uuid in ignored:
|
||||
continue
|
||||
if uuid in old_roles:
|
||||
continue
|
||||
if uuid == admin.uuid and not self.request.is_root:
|
||||
continue
|
||||
role = session.get(model.Role, uuid)
|
||||
user.roles.append(role)
|
||||
|
||||
# remove any roles which were *not* specified, taking care to
|
||||
# avoid certain unwanted operations for built-in roles
|
||||
for uuid in old_roles:
|
||||
if uuid in new_roles:
|
||||
continue
|
||||
if uuid == admin.uuid and not self.request.is_root:
|
||||
continue
|
||||
role = session.get(model.Role, uuid)
|
||||
user.roles.remove(role)
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue