feat: expose User "roles" for editing
This commit is contained in:
parent
bdfa0197b2
commit
97e914c2e0
|
@ -120,6 +120,13 @@ class Form:
|
||||||
|
|
||||||
See also :meth:`set_validator()`.
|
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
|
.. attribute:: readonly
|
||||||
|
|
||||||
Boolean indicating the form does not allow submit. In practice
|
Boolean indicating the form does not allow submit. In practice
|
||||||
|
@ -248,6 +255,7 @@ class Form:
|
||||||
nodes={},
|
nodes={},
|
||||||
widgets={},
|
widgets={},
|
||||||
validators={},
|
validators={},
|
||||||
|
defaults={},
|
||||||
readonly=False,
|
readonly=False,
|
||||||
readonly_fields=[],
|
readonly_fields=[],
|
||||||
required_fields={},
|
required_fields={},
|
||||||
|
@ -271,6 +279,7 @@ class Form:
|
||||||
self.nodes = nodes or {}
|
self.nodes = nodes or {}
|
||||||
self.widgets = widgets or {}
|
self.widgets = widgets or {}
|
||||||
self.validators = validators or {}
|
self.validators = validators or {}
|
||||||
|
self.defaults = defaults or {}
|
||||||
self.readonly = readonly
|
self.readonly = readonly
|
||||||
self.readonly_fields = set(readonly_fields or [])
|
self.readonly_fields = set(readonly_fields or [])
|
||||||
self.required_fields = required_fields or {}
|
self.required_fields = required_fields or {}
|
||||||
|
@ -375,6 +384,23 @@ class Form:
|
||||||
"""
|
"""
|
||||||
self.fields = FieldList(fields)
|
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):
|
def remove(self, *keys):
|
||||||
"""
|
"""
|
||||||
Remove some fields(s) from the form.
|
Remove some fields(s) from the form.
|
||||||
|
@ -471,6 +497,18 @@ class Form:
|
||||||
if self.schema and key in self.schema:
|
if self.schema and key in self.schema:
|
||||||
self.schema[key].validator = validator
|
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):
|
def set_readonly(self, key, readonly=True):
|
||||||
"""
|
"""
|
||||||
Enable or disable the "readonly" flag for a given field.
|
Enable or disable the "readonly" flag for a given field.
|
||||||
|
@ -624,30 +662,17 @@ class Form:
|
||||||
|
|
||||||
if self.model_class:
|
if self.model_class:
|
||||||
|
|
||||||
# first define full list of 'includes' - final schema
|
# collect list of field names and/or nodes
|
||||||
# should contain all of these fields
|
includes = []
|
||||||
includes = list(fields)
|
for key in fields:
|
||||||
|
|
||||||
# determine which we want ColanderAlchemy to handle
|
|
||||||
auto_includes = []
|
|
||||||
for key in includes:
|
|
||||||
|
|
||||||
# skip if we already have a node defined
|
|
||||||
if key in self.nodes:
|
if key in self.nodes:
|
||||||
continue
|
includes.append(self.nodes[key])
|
||||||
|
else:
|
||||||
# we want the magic for this field
|
includes.append(key)
|
||||||
auto_includes.append(key)
|
|
||||||
|
|
||||||
# make initial schema with ColanderAlchemy magic
|
# make initial schema with ColanderAlchemy magic
|
||||||
schema = SQLAlchemySchemaNode(self.model_class,
|
schema = SQLAlchemySchemaNode(self.model_class,
|
||||||
includes=auto_includes)
|
includes=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)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
@ -685,6 +710,11 @@ class Form:
|
||||||
elif key in schema: # field-level
|
elif key in schema: # field-level
|
||||||
schema[key].validator = validator
|
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
|
# apply required flags
|
||||||
for key, required in self.required_fields.items():
|
for key, required in self.required_fields.items():
|
||||||
if key in schema:
|
if key in schema:
|
||||||
|
|
|
@ -257,3 +257,44 @@ class PersonRef(ObjectRef):
|
||||||
def sort_query(self, query):
|
def sort_query(self, query):
|
||||||
""" """
|
""" """
|
||||||
return query.order_by(self.model_class.full_name)
|
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.TextInputWidget`
|
||||||
* :class:`deform:deform.widget.TextAreaWidget`
|
* :class:`deform:deform.widget.TextAreaWidget`
|
||||||
* :class:`deform:deform.widget.SelectWidget`
|
* :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 webhelpers2.html import HTML
|
||||||
|
|
||||||
|
from wuttaweb.db import Session
|
||||||
|
|
||||||
|
|
||||||
class ObjectRefWidget(SelectWidget):
|
class ObjectRefWidget(SelectWidget):
|
||||||
"""
|
"""
|
||||||
|
@ -96,3 +101,45 @@ class NotesWidget(TextAreaWidget):
|
||||||
* ``readonly/notes``
|
* ``readonly/notes``
|
||||||
"""
|
"""
|
||||||
readonly_template = '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)
|
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):
|
def remove(self, *keys):
|
||||||
"""
|
"""
|
||||||
Remove some column(s) from the grid.
|
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 json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
|
@ -420,13 +422,16 @@ def get_model_fields(config, model_class=None):
|
||||||
that to determine the field listing if applicable. Otherwise this
|
that to determine the field listing if applicable. Otherwise this
|
||||||
returns ``None``.
|
returns ``None``.
|
||||||
"""
|
"""
|
||||||
if model_class:
|
if not model_class:
|
||||||
import sqlalchemy as sa
|
return
|
||||||
|
|
||||||
app = config.get_app()
|
app = config.get_app()
|
||||||
model = app.model
|
model = app.model
|
||||||
if model_class and issubclass(model_class, model.Base):
|
if not issubclass(model_class, model.Base):
|
||||||
|
return
|
||||||
|
|
||||||
mapper = sa.inspect(model_class)
|
mapper = sa.inspect(model_class)
|
||||||
fields = list([prop.key for prop in mapper.iterate_properties])
|
fields = [prop.key for prop in mapper.iterate_properties]
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import colander
|
||||||
|
|
||||||
from wuttjamaican.db.model import User
|
from wuttjamaican.db.model import User
|
||||||
from wuttaweb.views import MasterView
|
from wuttaweb.views import MasterView
|
||||||
from wuttaweb.forms.schema import PersonRef
|
from wuttaweb.forms.schema import PersonRef, RoleRefs
|
||||||
from wuttaweb.db import Session
|
from wuttaweb.db import Session
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,10 +77,10 @@ class UserView(MasterView):
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
""" """
|
""" """
|
||||||
super().configure_form(f)
|
super().configure_form(f)
|
||||||
|
user = f.model_instance
|
||||||
|
|
||||||
# never show these
|
# never show these
|
||||||
f.remove('person_uuid',
|
f.remove('person_uuid',
|
||||||
'password',
|
|
||||||
'role_refs')
|
'role_refs')
|
||||||
|
|
||||||
# person
|
# person
|
||||||
|
@ -90,6 +90,18 @@ class UserView(MasterView):
|
||||||
# username
|
# username
|
||||||
f.set_validator('username', self.unique_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):
|
def unique_username(self, node, value):
|
||||||
""" """
|
""" """
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
@ -105,6 +117,62 @@ class UserView(MasterView):
|
||||||
if query.count():
|
if query.count():
|
||||||
node.raise_invalid("Username must be unique")
|
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):
|
def defaults(config, **kwargs):
|
||||||
base = globals()
|
base = globals()
|
||||||
|
|
|
@ -84,6 +84,12 @@ class TestForm(TestCase):
|
||||||
form.set_fields(['baz'])
|
form.set_fields(['baz'])
|
||||||
self.assertEqual(form.fields, ['baz'])
|
self.assertEqual(form.fields, ['baz'])
|
||||||
|
|
||||||
|
def test_append(self):
|
||||||
|
form = self.make_form(fields=['one', 'two'])
|
||||||
|
self.assertEqual(form.fields, ['one', 'two'])
|
||||||
|
form.append('one', 'two', 'three')
|
||||||
|
self.assertEqual(form.fields, ['one', 'two', 'three'])
|
||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
form = self.make_form(fields=['one', 'two', 'three', 'four'])
|
form = self.make_form(fields=['one', 'two', 'three', 'four'])
|
||||||
self.assertEqual(form.fields, ['one', 'two', 'three', 'four'])
|
self.assertEqual(form.fields, ['one', 'two', 'three', 'four'])
|
||||||
|
@ -157,6 +163,14 @@ class TestForm(TestCase):
|
||||||
self.assertIs(form.validators['foo'], validate2)
|
self.assertIs(form.validators['foo'], validate2)
|
||||||
self.assertIs(schema['foo'].validator, validate2)
|
self.assertIs(schema['foo'].validator, validate2)
|
||||||
|
|
||||||
|
def test_set_default(self):
|
||||||
|
form = self.make_form(fields=['foo', 'bar'])
|
||||||
|
self.assertEqual(form.defaults, {})
|
||||||
|
|
||||||
|
# basic
|
||||||
|
form.set_default('foo', 42)
|
||||||
|
self.assertEqual(form.defaults['foo'], 42)
|
||||||
|
|
||||||
def test_get_schema(self):
|
def test_get_schema(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
form = self.make_form()
|
form = self.make_form()
|
||||||
|
@ -233,6 +247,12 @@ class TestForm(TestCase):
|
||||||
schema = form.get_schema()
|
schema = form.get_schema()
|
||||||
self.assertIs(schema.validator, validate)
|
self.assertIs(schema.validator, validate)
|
||||||
|
|
||||||
|
# default value overrides are honored
|
||||||
|
form = self.make_form(model_class=model.Setting)
|
||||||
|
form.set_default('name', 'foo')
|
||||||
|
schema = form.get_schema()
|
||||||
|
self.assertEqual(schema['name'].default, 'foo')
|
||||||
|
|
||||||
def test_get_deform(self):
|
def test_get_deform(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
schema = self.make_schema()
|
schema = self.make_schema()
|
||||||
|
@ -422,6 +442,19 @@ class TestForm(TestCase):
|
||||||
# nb. no error message
|
# nb. no error message
|
||||||
self.assertNotIn('message', html)
|
self.assertNotIn('message', html)
|
||||||
|
|
||||||
|
def test_get_vue_model_data(self):
|
||||||
|
schema = self.make_schema()
|
||||||
|
form = self.make_form(schema=schema)
|
||||||
|
|
||||||
|
# 2 fields by default (foo, bar)
|
||||||
|
data = form.get_vue_model_data()
|
||||||
|
self.assertEqual(len(data), 2)
|
||||||
|
|
||||||
|
# still just 2 fields even if we request more
|
||||||
|
form.set_fields(['foo', 'bar', 'baz'])
|
||||||
|
data = form.get_vue_model_data()
|
||||||
|
self.assertEqual(len(data), 2)
|
||||||
|
|
||||||
def test_get_field_errors(self):
|
def test_get_field_errors(self):
|
||||||
schema = self.make_schema()
|
schema = self.make_schema()
|
||||||
form = self.make_form(schema=schema)
|
form = self.make_form(schema=schema)
|
||||||
|
|
|
@ -197,3 +197,27 @@ class TestPersonRef(DataTestCase):
|
||||||
sorted_query = typ.sort_query(query)
|
sorted_query = typ.sort_query(query)
|
||||||
self.assertIsInstance(sorted_query, orm.Query)
|
self.assertIsInstance(sorted_query, orm.Query)
|
||||||
self.assertIsNot(sorted_query, query)
|
self.assertIsNot(sorted_query, query)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRoleRefs(DataTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.setup_db()
|
||||||
|
self.request = testing.DummyRequest(wutta_config=self.config)
|
||||||
|
|
||||||
|
def test_widget_maker(self):
|
||||||
|
model = self.app.model
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
admin = auth.get_role_administrator(self.session)
|
||||||
|
authed = auth.get_role_authenticated(self.session)
|
||||||
|
anon = auth.get_role_anonymous(self.session)
|
||||||
|
blokes = model.Role(name="Blokes")
|
||||||
|
self.session.add(blokes)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# default values for widget includes all but: authed, anon
|
||||||
|
typ = mod.RoleRefs(self.request, session=self.session)
|
||||||
|
widget = typ.widget_maker()
|
||||||
|
self.assertEqual(len(widget.values), 2)
|
||||||
|
self.assertEqual(widget.values[0][1], "Administrator")
|
||||||
|
self.assertEqual(widget.values[1][1], "Blokes")
|
||||||
|
|
|
@ -4,8 +4,8 @@ import colander
|
||||||
import deform
|
import deform
|
||||||
from pyramid import testing
|
from pyramid import testing
|
||||||
|
|
||||||
from wuttaweb.forms import widgets
|
from wuttaweb.forms import widgets as mod
|
||||||
from wuttaweb.forms.schema import PersonRef
|
from wuttaweb.forms.schema import PersonRef, RoleRefs
|
||||||
from tests.util import WebTestCase
|
from tests.util import WebTestCase
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
|
|
||||||
# standard (editable)
|
# standard (editable)
|
||||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||||
widget = widgets.ObjectRefWidget(self.request)
|
widget = mod.ObjectRefWidget(self.request)
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
html = widget.serialize(field, person.uuid)
|
html = widget.serialize(field, person.uuid)
|
||||||
self.assertIn('<b-select ', html)
|
self.assertIn('<b-select ', html)
|
||||||
|
@ -33,7 +33,45 @@ class TestObjectRefWidget(WebTestCase):
|
||||||
# readonly
|
# readonly
|
||||||
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
|
||||||
node.model_instance = person
|
node.model_instance = person
|
||||||
widget = widgets.ObjectRefWidget(self.request)
|
widget = mod.ObjectRefWidget(self.request)
|
||||||
field = self.make_field(node)
|
field = self.make_field(node)
|
||||||
html = widget.serialize(field, person.uuid, readonly=True)
|
html = widget.serialize(field, person.uuid, readonly=True)
|
||||||
self.assertEqual(html.strip(), '<span>Betty Boop</span>')
|
self.assertEqual(html.strip(), '<span>Betty Boop</span>')
|
||||||
|
|
||||||
|
|
||||||
|
class TestRoleRefsWidget(WebTestCase):
|
||||||
|
|
||||||
|
def make_field(self, node, **kwargs):
|
||||||
|
# TODO: not sure why default renderer is in use even though
|
||||||
|
# pyramid_deform was included in setup? but this works..
|
||||||
|
kwargs.setdefault('renderer', deform.Form.default_renderer)
|
||||||
|
return deform.Field(node, **kwargs)
|
||||||
|
|
||||||
|
def test_serialize(self):
|
||||||
|
model = self.app.model
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
admin = auth.get_role_administrator(self.session)
|
||||||
|
blokes = model.Role(name="Blokes")
|
||||||
|
self.session.add(blokes)
|
||||||
|
self.session.commit()
|
||||||
|
|
||||||
|
# nb. we let the field construct the widget via our type
|
||||||
|
node = colander.SchemaNode(RoleRefs(self.request, session=self.session))
|
||||||
|
field = self.make_field(node)
|
||||||
|
widget = field.widget
|
||||||
|
|
||||||
|
# readonly values list includes admin
|
||||||
|
html = widget.serialize(field, {admin.uuid, blokes.uuid}, readonly=True)
|
||||||
|
self.assertIn(admin.name, html)
|
||||||
|
self.assertIn(blokes.name, html)
|
||||||
|
|
||||||
|
# editable values list *excludes* admin (by default)
|
||||||
|
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
||||||
|
self.assertNotIn(admin.uuid, html)
|
||||||
|
self.assertIn(blokes.uuid, html)
|
||||||
|
|
||||||
|
# but admin is included for root user
|
||||||
|
self.request.is_root = True
|
||||||
|
html = widget.serialize(field, {admin.uuid, blokes.uuid})
|
||||||
|
self.assertIn(admin.uuid, html)
|
||||||
|
self.assertIn(blokes.uuid, html)
|
||||||
|
|
|
@ -69,6 +69,12 @@ class TestGrid(TestCase):
|
||||||
self.assertEqual(grid.columns, ['name', 'value'])
|
self.assertEqual(grid.columns, ['name', 'value'])
|
||||||
self.assertEqual(grid.get_columns(), ['name', 'value'])
|
self.assertEqual(grid.get_columns(), ['name', 'value'])
|
||||||
|
|
||||||
|
def test_append(self):
|
||||||
|
grid = self.make_grid(columns=['one', 'two'])
|
||||||
|
self.assertEqual(grid.columns, ['one', 'two'])
|
||||||
|
grid.append('one', 'two', 'three')
|
||||||
|
self.assertEqual(grid.columns, ['one', 'two', 'three'])
|
||||||
|
|
||||||
def test_remove(self):
|
def test_remove(self):
|
||||||
grid = self.make_grid(columns=['one', 'two', 'three', 'four'])
|
grid = self.make_grid(columns=['one', 'two', 'three', 'four'])
|
||||||
self.assertEqual(grid.columns, ['one', 'two', 'three', 'four'])
|
self.assertEqual(grid.columns, ['one', 'two', 'three', 'four'])
|
||||||
|
|
|
@ -442,6 +442,14 @@ class TestGetModelFields(TestCase):
|
||||||
self.config = WuttaConfig()
|
self.config = WuttaConfig()
|
||||||
self.app = self.config.get_app()
|
self.app = self.config.get_app()
|
||||||
|
|
||||||
|
def test_empty_model_class(self):
|
||||||
|
fields = util.get_model_fields(self.config)
|
||||||
|
self.assertIsNone(fields)
|
||||||
|
|
||||||
|
def test_unknown_model_class(self):
|
||||||
|
fields = util.get_model_fields(self.config, TestCase)
|
||||||
|
self.assertIsNone(fields)
|
||||||
|
|
||||||
def test_basic(self):
|
def test_basic(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
fields = util.get_model_fields(self.config, model.Setting)
|
fields = util.get_model_fields(self.config, model.Setting)
|
||||||
|
|
|
@ -30,12 +30,30 @@ class TestUserView(WebTestCase):
|
||||||
|
|
||||||
def test_configure_form(self):
|
def test_configure_form(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
|
barney = model.User(username='barney')
|
||||||
|
self.session.add(barney)
|
||||||
|
self.session.commit()
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
form = view.make_form(model_class=model.Person)
|
|
||||||
|
# person is *not* required
|
||||||
|
with patch.object(view, 'creating', new=True):
|
||||||
|
form = view.make_form(model_class=model.User)
|
||||||
self.assertIsNone(form.is_required('person'))
|
self.assertIsNone(form.is_required('person'))
|
||||||
view.configure_form(form)
|
view.configure_form(form)
|
||||||
self.assertFalse(form.is_required('person'))
|
self.assertFalse(form.is_required('person'))
|
||||||
|
|
||||||
|
# password removed (always, for now)
|
||||||
|
with patch.object(view, 'viewing', new=True):
|
||||||
|
form = view.make_form(model_instance=barney)
|
||||||
|
self.assertIn('password', form)
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertNotIn('password', form)
|
||||||
|
with patch.object(view, 'editing', new=True):
|
||||||
|
form = view.make_form(model_instance=barney)
|
||||||
|
self.assertIn('password', form)
|
||||||
|
view.configure_form(form)
|
||||||
|
self.assertNotIn('password', form)
|
||||||
|
|
||||||
def test_unique_username(self):
|
def test_unique_username(self):
|
||||||
model = self.app.model
|
model = self.app.model
|
||||||
view = self.make_view()
|
view = self.make_view()
|
||||||
|
@ -55,3 +73,103 @@ class TestUserView(WebTestCase):
|
||||||
self.request.matchdict = {'uuid': user.uuid}
|
self.request.matchdict = {'uuid': user.uuid}
|
||||||
node = colander.SchemaNode(colander.String(), name='username')
|
node = colander.SchemaNode(colander.String(), name='username')
|
||||||
self.assertIsNone(view.unique_username(node, 'foo'))
|
self.assertIsNone(view.unique_username(node, 'foo'))
|
||||||
|
|
||||||
|
def test_objectify(self):
|
||||||
|
model = self.app.model
|
||||||
|
blokes = model.Role(name="Blokes")
|
||||||
|
self.session.add(blokes)
|
||||||
|
others = model.Role(name="Others")
|
||||||
|
self.session.add(others)
|
||||||
|
barney = model.User(username='barney')
|
||||||
|
barney.roles.append(blokes)
|
||||||
|
self.session.add(barney)
|
||||||
|
self.session.commit()
|
||||||
|
view = self.make_view()
|
||||||
|
view.editing = True
|
||||||
|
self.request.matchdict = {'uuid': barney.uuid}
|
||||||
|
|
||||||
|
# sanity check, user is just in 'blokes' role
|
||||||
|
self.session.refresh(barney)
|
||||||
|
self.assertEqual(len(barney.roles), 1)
|
||||||
|
self.assertEqual(barney.roles[0].name, "Blokes")
|
||||||
|
|
||||||
|
# form can update user roles
|
||||||
|
form = view.make_model_form(model_instance=barney)
|
||||||
|
form.validated = {'username': 'barney', 'roles': {others.uuid}}
|
||||||
|
user = view.objectify(form, session=self.session)
|
||||||
|
self.assertIs(user, barney)
|
||||||
|
self.assertEqual(len(user.roles), 1)
|
||||||
|
self.assertEqual(user.roles[0].name, "Others")
|
||||||
|
|
||||||
|
def test_update_roles(self):
|
||||||
|
model = self.app.model
|
||||||
|
auth = self.app.get_auth_handler()
|
||||||
|
admin = auth.get_role_administrator(self.session)
|
||||||
|
authed = auth.get_role_authenticated(self.session)
|
||||||
|
anon = auth.get_role_anonymous(self.session)
|
||||||
|
blokes = model.Role(name="Blokes")
|
||||||
|
self.session.add(blokes)
|
||||||
|
others = model.Role(name="Others")
|
||||||
|
self.session.add(others)
|
||||||
|
barney = model.User(username='barney')
|
||||||
|
barney.roles.append(blokes)
|
||||||
|
self.session.add(barney)
|
||||||
|
self.session.commit()
|
||||||
|
view = self.make_view()
|
||||||
|
view.editing = True
|
||||||
|
self.request.matchdict = {'uuid': barney.uuid}
|
||||||
|
|
||||||
|
# no error if data is missing roles
|
||||||
|
form = view.make_model_form(model_instance=barney)
|
||||||
|
form.validated = {'username': 'barneyx'}
|
||||||
|
user = view.objectify(form, session=self.session)
|
||||||
|
self.assertIs(user, barney)
|
||||||
|
self.assertEqual(barney.username, 'barneyx')
|
||||||
|
|
||||||
|
# sanity check, user is just in 'blokes' role
|
||||||
|
self.session.refresh(barney)
|
||||||
|
self.assertEqual(len(barney.roles), 1)
|
||||||
|
self.assertEqual(barney.roles[0].name, "Blokes")
|
||||||
|
|
||||||
|
# let's test a bunch at once to ensure:
|
||||||
|
# - user roles are updated
|
||||||
|
# - authed / anon roles are not added
|
||||||
|
# - admin role not added if current user is not root
|
||||||
|
form = view.make_model_form(model_instance=barney)
|
||||||
|
form.validated = {'username': 'barney',
|
||||||
|
'roles': {admin.uuid, authed.uuid, anon.uuid, others.uuid}}
|
||||||
|
user = view.objectify(form, session=self.session)
|
||||||
|
self.assertIs(user, barney)
|
||||||
|
self.assertEqual(len(user.roles), 1)
|
||||||
|
self.assertEqual(user.roles[0].name, "Others")
|
||||||
|
|
||||||
|
# let's test a bunch at once to ensure:
|
||||||
|
# - user roles are updated
|
||||||
|
# - admin role is added if current user is root
|
||||||
|
self.request.is_root = True
|
||||||
|
form = view.make_model_form(model_instance=barney)
|
||||||
|
form.validated = {'username': 'barney',
|
||||||
|
'roles': {admin.uuid, blokes.uuid, others.uuid}}
|
||||||
|
user = view.objectify(form, session=self.session)
|
||||||
|
self.assertIs(user, barney)
|
||||||
|
self.assertEqual(len(user.roles), 3)
|
||||||
|
role_uuids = set([role.uuid for role in user.roles])
|
||||||
|
self.assertEqual(role_uuids, {admin.uuid, blokes.uuid, others.uuid})
|
||||||
|
|
||||||
|
# admin role not removed if current user is not root
|
||||||
|
self.request.is_root = False
|
||||||
|
form = view.make_model_form(model_instance=barney)
|
||||||
|
form.validated = {'username': 'barney',
|
||||||
|
'roles': {blokes.uuid, others.uuid}}
|
||||||
|
user = view.objectify(form, session=self.session)
|
||||||
|
self.assertIs(user, barney)
|
||||||
|
self.assertEqual(len(user.roles), 3)
|
||||||
|
|
||||||
|
# admin role is removed if current user is root
|
||||||
|
self.request.is_root = True
|
||||||
|
form = view.make_model_form(model_instance=barney)
|
||||||
|
form.validated = {'username': 'barney',
|
||||||
|
'roles': {blokes.uuid, others.uuid}}
|
||||||
|
user = view.objectify(form, session=self.session)
|
||||||
|
self.assertIs(user, barney)
|
||||||
|
self.assertEqual(len(user.roles), 2)
|
||||||
|
|
Loading…
Reference in a new issue