diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py
index 7ee9b01..c667468 100644
--- a/src/wuttaweb/forms/base.py
+++ b/src/wuttaweb/forms/base.py
@@ -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:
diff --git a/src/wuttaweb/forms/schema.py b/src/wuttaweb/forms/schema.py
index ccb357f..dd46fb3 100644
--- a/src/wuttaweb/forms/schema.py
+++ b/src/wuttaweb/forms/schema.py
@@ -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)
diff --git a/src/wuttaweb/forms/widgets.py b/src/wuttaweb/forms/widgets.py
index bdd7666..f5c1523 100644
--- a/src/wuttaweb/forms/widgets.py
+++ b/src/wuttaweb/forms/widgets.py
@@ -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)
diff --git a/src/wuttaweb/grids/base.py b/src/wuttaweb/grids/base.py
index c0738f7..5d3f94b 100644
--- a/src/wuttaweb/grids/base.py
+++ b/src/wuttaweb/grids/base.py
@@ -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.
diff --git a/src/wuttaweb/templates/deform/checkbox_choice.pt b/src/wuttaweb/templates/deform/checkbox_choice.pt
new file mode 100644
index 0000000..f09b373
--- /dev/null
+++ b/src/wuttaweb/templates/deform/checkbox_choice.pt
@@ -0,0 +1,18 @@
+
+ ${field.start_sequence()}
+
+ ${field.end_sequence()}
+
diff --git a/src/wuttaweb/util.py b/src/wuttaweb/util.py
index 36942c3..02b5bcd 100644
--- a/src/wuttaweb/util.py
+++ b/src/wuttaweb/util.py
@@ -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):
diff --git a/src/wuttaweb/views/users.py b/src/wuttaweb/views/users.py
index 124aa59..6dd1a8f 100644
--- a/src/wuttaweb/views/users.py
+++ b/src/wuttaweb/views/users.py
@@ -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()
diff --git a/tests/forms/test_base.py b/tests/forms/test_base.py
index b3aade2..86b411a 100644
--- a/tests/forms/test_base.py
+++ b/tests/forms/test_base.py
@@ -84,6 +84,12 @@ class TestForm(TestCase):
form.set_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):
form = self.make_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(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):
model = self.app.model
form = self.make_form()
@@ -233,6 +247,12 @@ class TestForm(TestCase):
schema = form.get_schema()
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):
model = self.app.model
schema = self.make_schema()
@@ -422,6 +442,19 @@ class TestForm(TestCase):
# nb. no error message
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):
schema = self.make_schema()
form = self.make_form(schema=schema)
diff --git a/tests/forms/test_schema.py b/tests/forms/test_schema.py
index 3d2492b..02d69ce 100644
--- a/tests/forms/test_schema.py
+++ b/tests/forms/test_schema.py
@@ -197,3 +197,27 @@ class TestPersonRef(DataTestCase):
sorted_query = typ.sort_query(query)
self.assertIsInstance(sorted_query, orm.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")
diff --git a/tests/forms/test_widgets.py b/tests/forms/test_widgets.py
index d359030..e2ed0e7 100644
--- a/tests/forms/test_widgets.py
+++ b/tests/forms/test_widgets.py
@@ -4,8 +4,8 @@ import colander
import deform
from pyramid import testing
-from wuttaweb.forms import widgets
-from wuttaweb.forms.schema import PersonRef
+from wuttaweb.forms import widgets as mod
+from wuttaweb.forms.schema import PersonRef, RoleRefs
from tests.util import WebTestCase
@@ -25,7 +25,7 @@ class TestObjectRefWidget(WebTestCase):
# standard (editable)
node = colander.SchemaNode(PersonRef(self.request, session=self.session))
- widget = widgets.ObjectRefWidget(self.request)
+ widget = mod.ObjectRefWidget(self.request)
field = self.make_field(node)
html = widget.serialize(field, person.uuid)
self.assertIn('Betty Boop')
+
+
+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)
diff --git a/tests/grids/test_base.py b/tests/grids/test_base.py
index 89c5ab0..79f37ae 100644
--- a/tests/grids/test_base.py
+++ b/tests/grids/test_base.py
@@ -69,6 +69,12 @@ class TestGrid(TestCase):
self.assertEqual(grid.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):
grid = self.make_grid(columns=['one', 'two', 'three', 'four'])
self.assertEqual(grid.columns, ['one', 'two', 'three', 'four'])
diff --git a/tests/test_util.py b/tests/test_util.py
index 6fd94b7..f0f04af 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -442,6 +442,14 @@ class TestGetModelFields(TestCase):
self.config = WuttaConfig()
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):
model = self.app.model
fields = util.get_model_fields(self.config, model.Setting)
diff --git a/tests/views/test_users.py b/tests/views/test_users.py
index ea67544..292297b 100644
--- a/tests/views/test_users.py
+++ b/tests/views/test_users.py
@@ -30,11 +30,29 @@ class TestUserView(WebTestCase):
def test_configure_form(self):
model = self.app.model
+ barney = model.User(username='barney')
+ self.session.add(barney)
+ self.session.commit()
view = self.make_view()
- form = view.make_form(model_class=model.Person)
- self.assertIsNone(form.is_required('person'))
- view.configure_form(form)
- self.assertFalse(form.is_required('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'))
+ view.configure_form(form)
+ 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):
model = self.app.model
@@ -55,3 +73,103 @@ class TestUserView(WebTestCase):
self.request.matchdict = {'uuid': user.uuid}
node = colander.SchemaNode(colander.String(), name='username')
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)