From 8a074d665449775ffd9e0de26100dd35a5df8e63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 15 Jan 2021 14:38:28 +0100
Subject: [PATCH 1/2] tests: split admin_pages tests

---
 tests/admin_pages/test_all.py      | 1235 +---------------------------
 tests/admin_pages/test_role.py     |  134 +++
 tests/admin_pages/test_settings.py |  868 +++++++++++++++++++
 tests/admin_pages/test_user.py     |  344 ++++++++
 4 files changed, 1347 insertions(+), 1234 deletions(-)
 create mode 100644 tests/admin_pages/test_role.py
 create mode 100644 tests/admin_pages/test_settings.py
 create mode 100644 tests/admin_pages/test_user.py

diff --git a/tests/admin_pages/test_all.py b/tests/admin_pages/test_all.py
index 01ee9b46..3275c32a 100644
--- a/tests/admin_pages/test_all.py
+++ b/tests/admin_pages/test_all.py
@@ -1,41 +1,16 @@
 # -*- coding: utf-8 -*-
 
-import logging
 import os
-import zipfile
-
-try:
-    import lasso
-except ImportError:
-    lasso = None
 
 import pytest
-from webtest import Upload
-import mock
-
-from django.utils.six import BytesIO
-from django.utils.six.moves.urllib import parse as urlparse
-
-from quixote.http_request import Upload as QuixoteUpload
 
-from wcs.qommon.form import UploadedFile
 from wcs.qommon.ident.password_accounts import PasswordAccount
 from wcs.qommon.http_request import HTTPRequest
-from wcs.qommon.template import get_current_theme
-from wcs.admin.settings import UserFieldsFormDef
-from wcs.api_access import ApiAccess
-from wcs.categories import Category, CardDefCategory
-from wcs.data_sources import NamedDataSource
-from wcs.wscalls import NamedWsCall
 from wcs.roles import Role
-from wcs.workflows import (
-    Workflow, CommentableWorkflowStatusItem,
-    ChoiceWorkflowStatusItem, JumpOnSubmitWorkflowStatusItem)
-from wcs.wf.export_to_model import ExportToModel
+from wcs.workflows import Workflow, ChoiceWorkflowStatusItem, JumpOnSubmitWorkflowStatusItem
 from wcs.wf.redirect_to_url import RedirectToUrlWorkflowStatusItem
 from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
 from wcs.formdef import FormDef
-from wcs.carddef import CardDef
 from wcs import fields
 
 from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
@@ -201,1214 +176,6 @@ def test_users_roles_menu_entries(pub):
     assert 'Roles' not in [x['label'] for x in resp.json]
 
 
-def test_users(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-    app.get('/backoffice/users/')
-
-
-def test_users_new(pub):
-    pub.user_class.wipe()
-    create_superuser(pub)
-    user_count = pub.user_class.count()
-    account_count = PasswordAccount.count()
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/')
-    resp = resp.click('New User')
-    resp.forms[0]['name'] = 'a new user'
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/users/'
-    resp = resp.follow()
-    assert 'a new user' in resp.text
-    resp = resp.click('a new user')
-    assert 'User - a new user' in resp.text
-    assert pub.user_class.count() == user_count + 1
-    assert PasswordAccount.count() == account_count
-
-
-def test_users_new_with_account(pub):
-    pub.user_class.wipe()
-    create_superuser(pub)
-    user_count = pub.user_class.count()
-    account_count = PasswordAccount.count()
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/')
-    resp = resp.click('New User')
-    resp.forms[0]['name'] = 'a second user'
-    resp.forms[0]['method_password$username'] = 'second-user'
-    resp.forms[0]['method_password$password'] = 'foobar'
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/users/'
-    resp = resp.follow()
-    assert 'a second user' in resp.text
-    resp = resp.click('a second user')
-    assert 'User - a second user' in resp.text
-    assert pub.user_class.count() == user_count + 1
-    assert PasswordAccount.count() == account_count + 1
-
-
-def test_users_edit(pub):
-    pub.user_class.wipe()
-    create_superuser(pub)
-    user = pub.user_class(name='foo bar')
-    user.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/%s/' % user.id)
-    resp = resp.click(href='edit')
-    resp.forms[0]['is_admin'].checked = True
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
-    resp = resp.follow()
-
-
-def test_users_edit_new_account(pub):
-    pub.user_class.wipe()
-    PasswordAccount.wipe()
-    create_superuser(pub)
-    user = pub.user_class(name='foo bar')
-    user.store()
-    account_count = PasswordAccount.count()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/%s/' % user.id)
-    resp = resp.click(href='edit')
-    resp.forms[0]['is_admin'].checked = True
-    resp.forms[0]['method_password$username'] = 'foo'
-    resp.forms[0]['method_password$password'] = 'bar'
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
-    resp = resp.follow()
-
-    assert PasswordAccount.count() == account_count + 1
-
-
-def test_users_edit_edit_account(pub):
-    pub.user_class.wipe()
-    PasswordAccount.wipe()
-    create_superuser(pub)
-    user = pub.user_class(name='foo bar')
-    user.store()
-    account = PasswordAccount(id='test')
-    account.user_id = user.id
-    account.store()
-    assert PasswordAccount.has_key('test')
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/%s/' % user.id)
-    resp = resp.click(href='edit')
-    resp.forms[0]['is_admin'].checked = True
-    resp.forms[0]['method_password$username'] = 'foo' # change username
-    resp.forms[0]['method_password$password'] = 'bar'
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
-    resp = resp.follow()
-
-    # makes sure the old account has been removed
-    assert not PasswordAccount.has_key('test')
-    assert PasswordAccount.has_key('foo')
-    assert PasswordAccount.get('foo').user_id == user.id
-
-
-def test_users_edit_with_managing_idp(pub):
-    create_role()
-    pub.user_class.wipe()
-    pub.cfg['sp'] = {'idp-manage-user-attributes': True}
-    pub.write_cfg()
-    PasswordAccount.wipe()
-    create_superuser(pub)
-    user = pub.user_class(name='foo bar')
-    user.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/%s/' % user.id)
-    assert '>Manage Roles<' in resp.text
-    resp = resp.click(href='edit')
-    assert not 'email' in resp.form.fields
-    assert 'roles$add_element' in resp.form.fields
-
-    pub.cfg['sp'] = {'idp-manage-roles': True}
-    pub.write_cfg()
-    resp = app.get('/backoffice/users/%s/' % user.id)
-    assert '>Edit<' in resp.text
-    resp = resp.click(href='edit')
-    assert 'email' in resp.form.fields
-    assert 'roles$add_element' not in resp.form.fields
-
-    pub.cfg['sp'] = {'idp-manage-roles': True, 'idp-manage-user-attributes': True}
-    pub.write_cfg()
-    resp = app.get('/backoffice/users/%s/' % user.id)
-    assert not '/edit' in resp.text
-
-
-def test_users_delete(pub):
-    pub.user_class.wipe()
-    PasswordAccount.wipe()
-    create_superuser(pub)
-    user = pub.user_class(name='foo bar')
-    user.store()
-    account = PasswordAccount(id='test')
-    account.user_id = user.id
-    account.store()
-
-    user_count = pub.user_class.count()
-    account_count = PasswordAccount.count()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/%s/' % user.id)
-
-    resp = resp.click(href='delete')
-    resp = resp.forms[0].submit()
-    assert resp.location == 'http://example.net/backoffice/users/'
-    resp = resp.follow()
-
-    assert pub.user_class.count() == user_count - 1
-    assert PasswordAccount.count() == account_count - 1
-
-
-def test_users_view_deleted(pub):
-    pub.user_class.wipe()
-    PasswordAccount.wipe()
-    create_superuser(pub)
-    user = pub.user_class(name='foo bar')
-    user.store()
-    account = PasswordAccount(id='test')
-    account.user_id = user.id
-    account.store()
-
-    user.set_deleted()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/%s/' % user.id)
-    assert 'Marked as deleted on' in resp
-
-
-def test_users_pagination(pub):
-    pub.user_class.wipe()
-    PasswordAccount.wipe()
-    create_superuser(pub)
-    for i in range(50):
-        user = pub.user_class(name='foo bar %s' % (i+1))
-        user.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/')
-    assert 'foo bar 10' in resp.text
-    assert 'foo bar 30' not in resp.text
-
-    resp = resp.click('Next Page')
-    assert 'foo bar 10' not in resp.text
-    assert 'foo bar 30' in resp.text
-
-    resp = resp.click('Previous Page')
-    assert 'foo bar 10' in resp.text
-    assert 'foo bar 30' not in resp.text
-
-    resp = resp.click('Next Page')
-    resp = resp.click('Next Page')
-    assert 'foo bar 50' in resp.text
-
-
-def test_users_filter(pub):
-    pub.user_class.wipe()
-    PasswordAccount.wipe()
-    create_superuser(pub)
-    role = create_role()
-    for i in range(50):
-        user = pub.user_class(name='foo bar %s' % (i+1))
-        user.store()
-
-    for i in range(5):
-        user = pub.user_class(name='baz bar %s' % (i+1))
-        user.roles = [role.id]
-        user.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/')
-    assert 'admin' in resp.text # superuser
-    assert 'foo bar 10' in resp.text # simple user
-
-    # uncheck 'None'; unfortunately this doesn't work with webtest 1.3
-    #   resp.forms[0].fields['role'][-1].checked = False
-    #   resp = resp.forms[0].submit()
-    # therefore we fall back on using the URL
-    resp = app.get('/backoffice/users/?offset=0&limit=100&q=&filter=true&role=admin')
-    assert '>Number of filtered users: 1<' in resp.text
-    assert 'user-is-admin' in resp.text # superuser
-    assert 'foo bar 1' not in resp.text # simple user
-    assert 'baz bar 1' not in resp.text # user with role
-
-    resp = app.get('/backoffice/users/?offset=0&limit=100&q=&filter=true&role=1')
-    assert '>Number of filtered users: 5<' in resp.text
-    assert 'user-is-admin' not in resp.text # superuser
-    assert 'foo bar 10' not in resp.text # simple user
-    assert 'baz bar 1' in resp.text # user with role
-
-
-def test_users_search(pub):
-    pub.user_class.wipe()
-    PasswordAccount.wipe()
-    create_superuser(pub)
-    for i in range(20):
-        user = pub.user_class(name='foo %s' % (i+1))
-        user.store()
-    for i in range(10):
-        user = pub.user_class(name='bar %s' % (i+1))
-        user.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/')
-    assert 'foo 10' in resp.text
-
-    resp.forms[0]['q'] = 'bar'
-    resp = resp.forms[0].submit()
-    assert 'foo 10' not in resp.text
-    assert 'bar 10' in resp.text
-    assert 'Number of filtered users: 10' in resp.text
-
-
-def test_users_new_with_custom_formdef(pub):
-    pub.user_class.wipe()
-    formdef = UserFieldsFormDef(pub)
-    formdef.fields.append(fields.StringField(id='3', label='test', type='string'))
-    formdef.fields.append(fields.CommentField(id='4', label='test', type='comment'))
-    formdef.fields.append(fields.FileField(id='5', label='test', type='file', required=False))
-    formdef.store()
-
-    create_superuser(pub)
-    user_count = pub.user_class.count()
-    account_count = PasswordAccount.count()
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/')
-    resp = resp.click('New User')
-    resp.form['name'] = 'a new user'
-    resp.form['f3'] = 'TEST'
-    resp = resp.form.submit('submit')
-    assert resp.location == 'http://example.net/backoffice/users/'
-    resp = resp.follow()
-    assert 'a new user' in resp.text
-    resp = resp.click('a new user')
-    assert 'User - a new user' in resp.text
-    assert 'TEST' in resp.text
-    assert pub.user_class.count() == user_count + 1
-    assert PasswordAccount.count() == account_count
-
-
-def test_users_display_roles(pub):
-    pub.user_class.wipe()
-
-    user = create_superuser(pub)
-    role = create_role()
-    user.roles = [role.id, 'XXX']
-    user.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/users/%s/' % user.id)
-    assert role.name in resp.text
-    assert 'Unknown role (XXX)' in resp.text
-
-
-def test_roles(pub):
-    app = login(get_app(pub))
-    app.get('/backoffice/roles/')
-
-
-def test_roles_new(pub):
-    Role.wipe()
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/roles/')
-    resp = resp.click('New Role')
-    resp.forms[0]['name'] = 'a new role'
-    resp.forms[0]['details'] = 'bla bla bla'
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/roles/'
-    resp = resp.follow()
-    assert 'a new role' in resp.text
-    resp = resp.click('a new role')
-    assert '<h2>a new role' in resp.text
-
-    assert Role.get(1).name == 'a new role'
-    assert Role.get(1).details == 'bla bla bla'
-
-
-def test_roles_edit(pub):
-    Role.wipe()
-    role = Role(name='foobar')
-    role.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/roles/1/')
-    assert 'Holders of this role are granted access to the backoffice' in resp.text
-
-    resp = resp.click(href='edit')
-    assert resp.forms[0]['name'].value == 'foobar'
-    resp.forms[0]['name'] = 'baz'
-    resp.forms[0]['details'] = 'bla bla bla'
-    resp.forms[0]['emails_to_members'].checked = True
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/roles/1/'
-    resp = resp.follow()
-    assert '<h2>baz' in resp.text
-    assert 'Holders of this role will receive all emails adressed to the role.' in resp.text
-
-    assert Role.get(1).details == 'bla bla bla'
-    assert Role.get(1).emails_to_members == True
-
-
-def test_roles_matching_formdefs(pub):
-    Role.wipe()
-    role = Role(name='foo')
-    role.store()
-
-    FormDef.wipe()
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/roles/1/')
-    assert 'form bar' not in resp.text
-
-    formdef = FormDef()
-    formdef.name = 'form bar'
-    formdef.roles = [role.id]
-    formdef.fields = []
-    formdef.store()
-
-    resp = app.get('/backoffice/roles/1/')
-    assert 'form bar' in resp.text
-    assert 'form baz' not in resp.text
-
-    FormDef.wipe()
-    formdef = FormDef()
-    formdef.name = 'form baz'
-    formdef.fields = []
-    formdef.workflow_roles = {'_receiver': role.id}
-    formdef.store()
-
-    resp = app.get('/backoffice/roles/1/')
-    assert 'form baz' in resp.text
-    assert 'form bar' not in resp.text
-
-
-def test_roles_delete(pub):
-    Role.wipe()
-    role = Role(name='foobar')
-    role.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/roles/1/')
-
-    resp = resp.click(href='delete')
-    resp = resp.forms[0].submit()
-    assert resp.location == 'http://example.net/backoffice/roles/'
-    resp = resp.follow()
-    assert Role.count() == 0
-
-
-def test_settings(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-    app.get('/backoffice/settings/')
-
-    app.get('/backoffice/settings/debug_options')
-    app.get('/backoffice/settings/language')
-    app.get('/backoffice/settings/import')
-    app.get('/backoffice/settings/export')
-    app.get('/backoffice/settings/identification')
-    app.get('/backoffice/settings/sitename')
-    app.get('/backoffice/settings/sms')
-    app.get('/backoffice/settings/session')
-    app.get('/backoffice/settings/admin-permissions')
-
-
-def test_settings_disabled_screens(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/')
-    assert 'Identification' in resp.text
-    assert 'Theme' in resp.text
-
-    if not pub.site_options.has_section('options'):
-        pub.site_options.add_section('options')
-    pub.site_options.set('options', 'settings-disabled-screens', 'identification, theme')
-    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
-        pub.site_options.write(fd)
-    resp = app.get('/backoffice/settings/')
-    assert 'Identification' not in resp.text
-    assert 'Theme' not in resp.text
-
-
-def test_settings_export_import(pub):
-    def wipe():
-        FormDef.wipe()
-        CardDef.wipe()
-        Workflow.wipe()
-        Role.wipe()
-        Category.wipe()
-        CardDefCategory.wipe()
-        NamedDataSource.wipe()
-        NamedWsCall.wipe()
-        ApiAccess.wipe()
-
-    wipe()
-    create_superuser(pub)
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/export')
-    resp = resp.form.submit('cancel')
-    resp = app.get('/backoffice/settings/export')
-    resp = resp.form.submit('submit')
-    assert resp.location.startswith('http://example.net/backoffice/settings/export?job=')
-    job_id = urlparse.parse_qs(urlparse.urlparse(resp.location).query)['job'][0]
-    resp = resp.follow()
-    assert 'completed' in resp.text
-    resp = resp.click('Download Export')
-    zip_content = BytesIO(resp.body)
-    zipf = zipfile.ZipFile(zip_content, 'a')
-    filelist = zipf.namelist()
-    assert len(filelist) == 0
-
-    # check afterjob ajax call
-    status_resp = app.get('/afterjobs/' + job_id)
-    assert status_resp.text == 'completed|completed'
-
-    formdef = FormDef()
-    formdef.name = 'foo'
-    formdef.store()
-    carddef = CardDef()
-    carddef.name = 'bar'
-    carddef.store()
-    Category(name='baz').store()
-    CardDefCategory(name='foobar').store()
-    Role(name='qux').store()
-    NamedDataSource(name='quux').store()
-    NamedWsCall(name='corge').store()
-
-    wf = Workflow(name='bar')
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.label = 'test'
-    upload = QuixoteUpload('/foo/bar', content_type='application/vnd.oasis.opendocument.text')
-    file_content = b'''PK\x03\x04\x14\x00\x00\x08\x00\x00\'l\x8eG^\xc62\x0c\'\x00'''
-    upload.fp = BytesIO()
-    upload.fp.write(file_content)
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile('models', 'export_to_model-1.upload', upload)
-    st1.items.append(export_to)
-    export_to.parent = st1
-    wf.store()
-
-    api_access = ApiAccess()
-    api_access.name = 'Jhon'
-    api_access.api_identifier = 'jhon'
-    api_access.api_key = '1234'
-    api_access.store()
-
-    resp = app.get('/backoffice/settings/export')
-    resp = resp.form.submit('submit')
-    assert resp.location.startswith('http://example.net/backoffice/settings/export?job=')
-    job_id = urlparse.parse_qs(urlparse.urlparse(resp.location).query)['job'][0]
-    resp = resp.follow()
-    resp = resp.click('Download Export')
-    zip_content = BytesIO(resp.body)
-    zipf = zipfile.ZipFile(zip_content, 'a')
-    filelist = zipf.namelist()
-    assert 'formdefs/1' not in filelist
-    assert 'formdefs_xml/1' in filelist
-    assert 'carddefs/1' not in filelist
-    assert 'carddefs_xml/1' in filelist
-    assert 'workflows/1' not in filelist
-    assert 'workflows_xml/1' in filelist
-    assert 'models/export_to_model-1.upload' not in filelist
-    assert 'roles/1' in filelist
-    assert 'categories/1' in filelist
-    assert 'carddef_categories/1' in filelist
-    assert 'datasources/1' in filelist
-    assert 'wscalls/corge' in filelist
-    assert 'apiaccess/1' in filelist
-    for filename in filelist:
-        assert '.indexes' not in filename
-
-    wipe()
-    assert FormDef.count() == 0
-
-    resp = app.get('/backoffice/settings/import')
-    resp = resp.form.submit('cancel')
-
-    resp = app.get('/backoffice/settings/import')
-    resp.form['file'] = Upload('export.wcs', b'invalid content')
-    resp = resp.form.submit('submit')
-    assert 'Error: Not a valid export file' in resp.text
-
-    resp = app.get('/backoffice/settings/import')
-    resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
-    resp = resp.form.submit('submit')
-    assert 'Imported successfully' in resp.text
-    assert '1 forms' in resp.text
-    assert '1 cards' in resp.text
-    assert FormDef.count() == 1
-    assert FormDef.select()[0].url_name == 'foo'
-    assert CardDef.count() == 1
-    assert CardDef.select()[0].url_name == 'bar'
-    assert ApiAccess.count() == 1
-
-    # check roles are found by name
-    wipe()
-    role = Role(name='qux')
-    role.store()
-
-    workflow = Workflow(name='Workflow One')
-    st1 = workflow.add_status(name='st1')
-    commentable = CommentableWorkflowStatusItem()
-    commentable.id = '_commentable'
-    commentable.by = [role.id]
-    commentable.label = 'foobar'
-    st1.items.append(commentable)
-    commentable.parent = st1
-    workflow.store()
-
-    formdef = FormDef()
-    formdef.name = 'foo'
-    formdef.workflow_id = workflow.id
-    formdef.roles = [role.id]
-    formdef.backoffice_submission_roles = [role.id]
-    formdef.workflow_roles = {'_receiver': role.id}
-    formdef.fields = [fields.StringField(
-        id='1', type='string',
-        data_source={'type': 'carddef:unknown'})]
-    formdef.store()
-
-    resp = app.get('/backoffice/settings/export')
-    resp.form['formdefs'] = True
-    resp.form['workflows'] = True
-    resp.form['roles'] = False
-    resp.form['categories'] = False
-    resp.form['datasources'] = False
-    resp.form['wscalls'] = False
-    resp = resp.form.submit('submit')
-    assert resp.location.startswith('http://example.net/backoffice/settings/export?job=')
-    job_id = urlparse.parse_qs(urlparse.urlparse(resp.location).query)['job'][0]
-    resp = resp.follow()
-    resp = resp.click('Download Export')
-    zip_content = BytesIO(resp.body)
-    zipf = zipfile.ZipFile(zip_content, 'a')
-    filelist = zipf.namelist()
-    assert 'formdefs_xml/%s' % formdef.id in filelist
-    assert 'workflows_xml/%s' % workflow.id in filelist
-    assert 'roles/%s' % role.id not in filelist
-
-    FormDef.wipe()
-    Workflow.wipe()
-    Role.wipe()
-
-    # create role beforehand, it should be matched by name
-    role = Role(name='qux')
-    role.id = '012345'
-    role.store()
-
-    resp = app.get('/backoffice/settings/import')
-    resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
-    resp = resp.form.submit('submit')
-    assert FormDef.select()[0].roles == ['012345']
-    assert FormDef.select()[0].backoffice_submission_roles == ['012345']
-    assert FormDef.select()[0].workflow_roles == {'_receiver': '012345'}
-    assert len(FormDef.select()[0].fields) == 1
-    assert Workflow.select()[0].possible_status[0].items[0].by == ['012345']
-
-    # do not export roles when managed by idp
-    pub.cfg['sp'] = {'idp-manage-roles': True}
-    pub.write_cfg()
-    resp = app.get('/backoffice/settings/export')
-    resp = resp.form.submit('submit')
-    assert resp.location.startswith('http://example.net/backoffice/settings/export?job=')
-    job_id = urlparse.parse_qs(urlparse.urlparse(resp.location).query)['job'][0]
-    resp = resp.follow()
-    resp = resp.click('Download Export')
-    zip_content = BytesIO(resp.body)
-    zipf = zipfile.ZipFile(zip_content, 'a')
-    filelist = zipf.namelist()
-    assert len([x for x in filelist if 'roles/' in x]) == 0
-
-    # check an error is displayed if such an import is then used and roles are
-    # missing.
-    FormDef.wipe()
-    Workflow.wipe()
-    Role.wipe()
-    resp = app.get('/backoffice/settings/import')
-    resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
-    resp = resp.form.submit('submit')
-    assert 'Unknown referenced role (qux)' in resp
-
-
-def test_settings_themes(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-
-    # create mock theme
-    os.mkdir(os.path.join(pub.app_dir, 'themes'))
-    os.mkdir(os.path.join(pub.app_dir, 'themes', 'test'))
-    fd = open(os.path.join(pub.app_dir, 'themes', 'test', 'desc.xml'), 'w')
-    fd.write('<?xml version="1.0"?>'\
-             '<theme name="test" version="1.0">'\
-             '  <label>Test Theme</label>'\
-             '</theme>')
-    fd.close()
-
-    resp = app.get('/backoffice/settings/themes')
-    assert 'biglist themes' in resp.text
-    assert 'Test Theme (1.0)' in resp.text
-
-    # just for the kick, there's no support for uploading file in webtest 1.3
-    resp = app.get('/backoffice/settings/themes')
-    resp.click('Install New Theme')
-
-    # select the theme
-    resp = app.get('/backoffice/settings/themes')
-    resp.forms[0]['theme'].value = 'test'
-    resp = resp.forms[0].submit()
-    assert resp.location == 'http://example.net/backoffice/settings/'
-
-    resp = app.get('/backoffice/settings/themes')
-    assert 'checked' in resp.text
-    assert get_current_theme()['name'] == 'test'
-
-
-def test_settings_template(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/template')
-
-    # change template
-    orig_value = resp.forms[0]['template'].value
-    assert not 'foobar' in orig_value
-    resp.forms[0]['template'] = orig_value + '<!-- foobar -->'
-    resp = resp.forms[0].submit('submit')
-
-    # restore default template
-    resp = app.get('/backoffice/settings/template')
-    assert 'foobar' in resp.forms[0]['template'].value
-    resp = resp.forms[0].submit('restore-default')
-
-    # check
-    resp = app.get('/backoffice/settings/template')
-    assert resp.forms[0]['template'].value == orig_value
-
-
-def test_settings_user(pub):
-    user = create_superuser(pub)
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/users').follow().follow()
-
-    # add a field
-    resp.forms[2]['label'] = 'foobar'
-    resp = resp.forms[2].submit()
-    assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
-    resp = resp.follow()
-    assert b'foobar' in pub.cfg['users']['formdef']
-    assert 'foobar' in resp.text
-
-    # set field as email
-    resp.forms['mapping']['field_email'] = '1'
-    resp = resp.forms['mapping'].submit()
-    assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
-    resp = resp.follow()
-    assert pub.cfg['users']['field_email'] == '1'
-
-    # and unset it
-    resp.forms['mapping']['field_email'] = ''
-    resp = resp.forms['mapping'].submit()
-    assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
-    resp = resp.follow()
-    assert pub.cfg['users']['field_email'] == None
-
-    # add a comment field
-    resp.forms[2]['label'] = 'barfoo'
-    resp.forms[2]['type'] = 'comment'
-    resp = resp.forms[2].submit()
-    assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
-    resp = resp.follow()
-    assert b'barfoo' in pub.cfg['users']['formdef']
-    assert 'barfoo' in resp.text
-
-    # check fields are present in edit form
-    resp = app.get('/backoffice/users/%s/edit' % user.id)
-    assert 'barfoo' in resp.text
-    assert 'f1' in resp.form.fields
-    assert 'email' in resp.form.fields
-
-    # check the email field is not displayed if it's overridden by a custom
-    # field.
-    pub.cfg['users']['field_email'] = '1'
-    pub.write_cfg()
-    resp = app.get('/backoffice/users/%s/edit' % user.id)
-    assert 'f1' in resp.form.fields
-    assert 'email' not in resp.form.fields
-
-    # set a sidebar template
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/users').follow().follow()
-    resp.forms['template']['sidebar_template'] = 'hello {{ form_user_display_name }}'
-    resp = resp.forms['template'].submit().follow()
-    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
-    resp.forms['template']['sidebar_template'] = '{% if True %}'
-    resp = resp.forms['template'].submit().follow()
-    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
-    assert 'syntax error in Django template' in resp
-
-    # disable users screen
-    if not pub.site_options.has_section('options'):
-        pub.site_options.add_section('options')
-    pub.site_options.set('options', 'settings-disabled-screens', 'users')
-    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
-        pub.site_options.write(fd)
-
-    resp = app.get('/backoffice/settings/')
-    resp = resp.click('Users', href='user-template')
-    resp.forms['template']['sidebar_template'] = '{% if True %}'
-    resp = resp.forms['template'].submit()
-    assert 'syntax error in Django template' in resp
-    resp.forms['template']['sidebar_template'] = 'hello {{ form_user_display_name }}'
-    resp = resp.forms['template'].submit()
-    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
-
-    # restore config
-    pub.cfg['users']['field_email'] = None
-    pub.write_cfg()
-
-
-def test_settings_emails(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-
-    pub.cfg['debug'] = {'mail_redirection': 'foo@example.net'}
-    pub.write_cfg()
-    resp = app.get('/backoffice/settings/emails/')
-    resp = resp.click('General Options')
-    assert 'Warning: all emails are sent to &lt;foo@example.net&gt;' in resp.text
-    resp.form['from'] = 'test@localhost'
-    resp = resp.form.submit('submit')
-    pub.reload_cfg()
-    assert pub.cfg['emails']['from'] == 'test@localhost'
-    assert pub.cfg['emails']['well_known_domains']
-    assert pub.cfg['emails']['valid_known_domains']
-
-    pub.cfg['debug'] = {}
-    pub.write_cfg()
-    resp = app.get('/backoffice/settings/emails/')
-    resp = resp.click('General Options')
-    assert 'Warning: all emails are sent to &lt;foo@example.net&gt;' not in resp.text
-
-    resp = app.get('/backoffice/settings/emails/')
-    resp = resp.click('Approval of new account')
-    resp.forms[0]['email-new-account-approved_subject'] = 'bla'
-    resp.forms[0]['email-new-account-approved'] = 'bla bla bla'
-    resp = resp.forms[0].submit()
-    assert pub.cfg['emails']['email-new-account-approved_subject'] == 'bla'
-    assert pub.cfg['emails']['email-new-account-approved'] == 'bla bla bla'
-
-    # reset to default value
-    resp = app.get('/backoffice/settings/emails/')
-    resp = resp.click('Approval of new account')
-    resp.forms[0]['email-new-account-approved_subject'] = 'Your account has been approved'
-    resp = resp.forms[0].submit()
-    assert pub.cfg['emails']['email-new-account-approved_subject'] is None
-
-
-def test_settings_texts(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-
-    resp = app.get('/backoffice/settings/texts/')
-    resp = resp.click('Text on top of the login page')
-    resp.forms[0]['text-top-of-login'] = 'Hello world'
-    resp = resp.forms[0].submit()
-    assert resp.location == 'http://example.net/backoffice/settings/texts/'
-    assert pub.cfg['texts']['text-top-of-login'] == 'Hello world'
-
-    resp = app.get('/backoffice/settings/texts/')
-    resp = resp.click('Text on top of the login page')
-    resp = resp.forms[0].submit('restore-default')
-    assert resp.location == 'http://example.net/backoffice/settings/texts/'
-    assert pub.cfg['texts']['text-top-of-login'] == None
-
-
-@pytest.mark.skipif('lasso is None')
-def test_settings_auth(pub):
-    pub.user_class.wipe() # makes sure there are no users
-    pub.cfg['identification'] = {}
-    pub.write_cfg()
-    app = get_app(pub)
-
-    resp = app.get('/backoffice/settings/')
-    assert not 'identification/password/' in resp.text
-    assert not 'identification/idp/' in resp.text
-
-    resp = resp.click('Identification')
-    assert resp.forms[0]['methods$elementidp'].checked is False
-    assert resp.forms[0]['methods$elementpassword'].checked is False
-    resp.forms[0]['methods$elementidp'].checked = True
-    resp = resp.forms[0].submit()
-    resp = resp.follow()
-    assert 'identification/idp/' in resp.text
-    assert pub.cfg['identification']['methods'] == ['idp']
-
-    resp = resp.click('Identification')
-    assert resp.forms[0]['methods$elementidp'].checked is True
-    assert resp.forms[0]['methods$elementpassword'].checked is False
-    resp.forms[0]['methods$elementidp'].checked = False
-    resp.forms[0]['methods$elementpassword'].checked = True
-    resp = resp.forms[0].submit()
-    resp = resp.follow()
-    assert 'identification/password/' in resp.text
-    assert pub.cfg['identification']['methods'] == ['password']
-
-
-@pytest.mark.skipif('lasso is None')
-def test_settings_idp(pub):
-    # create admin session
-    create_superuser(pub)
-    app = login(get_app(pub))
-
-    pub.cfg['identification'] = {'methods': ['idp']}
-    pub.write_cfg()
-    app.get('/saml/metadata', status=404)
-    resp = app.get('/backoffice/settings/')
-    resp = resp.click(href='identification/idp/')
-    resp = resp.click('Service Provider')
-    resp = resp.form.submit('generate_rsa').follow()
-    resp = resp.form.submit('submit')
-    resp = resp.follow()
-
-    resp2 = resp.click('Identities')
-    resp2 = resp2.form.submit('cancel').follow()
-    resp2 = resp.click('Identities')
-    resp2 = resp2.form.submit('submit')
-
-    resp_metadata = app.get('/saml/metadata', status=200)
-    assert resp_metadata.text.startswith('<?xml')
-    resp2 = resp.click('Identity Providers')
-    resp2 = resp2.click('New')
-    idp_metadata_filename = os.path.join(os.path.dirname(__file__), '..', 'idp_metadata.xml')
-    resp2.form['metadata'] = Upload('idp_metadata.xml', open(idp_metadata_filename, 'rb').read())
-    resp2 = resp2.form.submit('submit')
-
-    resp = resp.click('Identity Providers')
-    assert 'http://authentic.example.net/' in resp.text
-    resp2 = resp.click(href='http-authentic.example.net-idp-saml2-metadata/', index=0)
-    assert 'ns0:EntityDescriptor' in resp2.text
-    resp = resp.click(href='http-authentic.example.net-idp-saml2-metadata/edit')
-    resp = resp.forms[0].submit('submit')
-    resp = resp.follow()
-
-    # test that login initiates a SSO
-    login_resp = app.get('/login/', status=302)
-    assert login_resp.location.startswith('http://authentic.example.net/idp/saml2/sso?SAMLRequest')
-
-    resp = resp.click(href='/backoffice/settings/identification/idp/idp/', index=0) # breadcrumb
-    resp = resp.click(href='http-authentic.example.net-idp-saml2-metadata/delete')
-    resp = resp.forms[0].submit() # confirm delete
-    assert len(pub.cfg['idp']) == 0
-
-    with mock.patch('wcs.qommon.misc.urlopen') as urlopen:
-        idp_metadata_filename = os.path.join(os.path.dirname(__file__), '..', 'idp_metadata.xml')
-        urlopen.side_effect = lambda *args: open(idp_metadata_filename, 'rb')
-        resp = app.get('/backoffice/settings/identification/idp/idp/')
-        resp = resp.click('Create new from remote URL')
-        resp.form['metadata_url'] = 'http://authentic.example.net/idp/saml2/metadata'
-        resp = resp.form.submit('submit')
-        resp = resp.follow()
-        assert 'http://authentic.example.net/idp/saml2/metadata' in resp.text
-        assert urlopen.call_count == 1
-        resp = resp.click('View')
-        resp = resp.click('Update from remote URL')
-        assert urlopen.call_count == 2
-
-
-def test_settings_auth_password(pub):
-    Role.wipe()
-
-    pub.user_class.wipe() # makes sure there are no users
-    pub.cfg['identification'] = {'methods': ['password']}
-    assert pub.cfg['identification']['methods'] == ['password']
-    pub.write_cfg()
-    app = get_app(pub)
-
-    resp = app.get('/backoffice/settings/identification/password/')
-    resp = resp.click('Identities')
-    resp = resp.forms[0].submit()
-
-    resp = app.get('/backoffice/settings/identification/password/')
-    resp = resp.click('Passwords')
-    resp = resp.forms[0].submit()
-
-
-def test_settings_filetypes(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-
-    resp = app.get('/backoffice/settings/filetypes/')
-    assert 'There are no file type defined at the moment.' in resp.text
-    resp.forms[0]['label'] = 'Text files'
-    resp.forms[0]['mimetypes'] = '.odt'
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/settings/filetypes/'
-    resp = resp.follow()
-    assert pub.cfg['filetypes'][1]['label'] == 'Text files'
-
-    resp = resp.click('Text files')
-    assert resp.forms[0]['mimetypes'].value == 'application/vnd.oasis.opendocument.text'
-    resp.forms[0]['mimetypes'] = 'application/vnd.oasis.opendocument.text, .doc, .docx, .pdf'
-    resp = resp.forms[0].submit('submit')
-    assert resp.location == 'http://example.net/backoffice/settings/filetypes/'
-    resp = resp.follow()
-    assert 'application/msword (.' in resp.text
-    assert 'application/pdf' in pub.cfg['filetypes'][1]['mimetypes']
-
-    resp.forms[0]['label'] = 'HTML files'
-    resp.forms[0]['mimetypes'] = '.html'
-    resp = resp.forms[0].submit('submit')
-    resp = resp.follow()
-    resp = resp.click('HTML files') # go to form
-    resp = resp.forms[0].submit('cancel') # and cancel
-    assert resp.location == 'http://example.net/backoffice/settings/filetypes/'
-    resp = resp.follow()
-    assert 'HTML files' in resp.text
-
-    resp = resp.click('HTML files') # go to form
-    resp = resp.forms[0].submit('delete') # and delete
-    assert resp.location == 'http://example.net/backoffice/settings/filetypes/'
-    resp = resp.follow()
-    assert 'HTML files' not in resp.text
-
-
-def test_settings_filetypes_update(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-
-    pub.cfg['filetypes'] = {
-            1: {'mimetypes': ['application/pdf', 'application/msword'],
-                'label': 'Text files'}
-            }
-    pub.write_cfg()
-    resp = app.get('/backoffice/settings/filetypes/')
-    assert 'Text files' in resp.text
-
-    FormDef.wipe()
-    formdef = FormDef()
-    formdef.name = 'form title'
-    formdef.fields = [fields.FileField(
-            id='1', label='1st field', type='file',
-            document_type={
-                'id': 1,
-                'mimetypes': ['application/pdf', 'application/msword'],
-                'label': 'Text files',
-            })]
-    formdef.store()
-    assert FormDef.get(formdef.id).fields[0].document_type == {
-                'id': 1,
-                'mimetypes': ['application/pdf', 'application/msword'],
-                'label': 'Text files',
-            }
-
-    resp = resp.click('Text files')
-    resp.forms[0]['mimetypes'] = 'application/vnd.oasis.opendocument.text, .doc, .docx, .pdf'
-    resp = resp.forms[0].submit('submit')
-    assert 'application/pdf' in pub.cfg['filetypes'][1]['mimetypes']
-    assert FormDef.get(formdef.id).fields[0].document_type == {
-                'id': 1,
-                'mimetypes': ['application/vnd.oasis.opendocument.text',
-                              'application/msword',
-                              'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-                              'application/pdf' ],
-                'label': 'Text files',
-            }
-
-
-def test_settings_logs(pub):
-    # reset logging state
-    logging.shutdown()
-    if os.path.exists(os.path.join(pub.app_dir, 'wcs.log')):
-        os.unlink(os.path.join(pub.app_dir, 'wcs.log'))
-
-    create_superuser(pub)
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/')
-    assert 'Logs' in resp.text
-    resp = resp.click('Logs')
-    assert '<td class="message">login</td>' in resp.text
-
-    resp = app.get('/backoffice/settings/debug_options')
-    assert resp.form['logger'].checked is True
-    resp.form['logger'].checked = False
-    resp = resp.form.submit()
-    resp = resp.follow()
-    assert 'Logs' not in resp.text
-
-
-def test_settings_geolocation(pub):
-    create_superuser(pub)
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/')
-    resp = resp.click('Geolocation')
-    resp.form['default-position$latlng'].value = '1.234;-1.234'
-    resp = resp.form.submit('cancel').follow()
-    resp = resp.click('Geolocation')
-    assert not 'value="1.234;-1.234' in resp.text
-    resp.form['default-position$latlng'].value = '1.234;-1.234'
-    resp = resp.form.submit().follow()
-    resp = resp.click('Geolocation')
-    assert 'value="1.234;-1.234' in resp.text
-    pub.reload_cfg()
-    assert pub.cfg['misc']['default-position'] == '1.234;-1.234'
-
-
-def test_settings_permissions(pub):
-    create_superuser(pub)
-    role1 = create_role()
-    role1.name = 'foobar1'
-    role1.store()
-    role2 = Role(name='foobar2')
-    role2.store()
-    role3 = Role(name='foobar3')
-    role3.store()
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/admin-permissions')
-    # assert all first checkboxes are checked
-    assert resp.forms[0]['permissions$c-0-0'].checked
-    assert resp.forms[0]['permissions$c-1-0'].checked
-    assert resp.forms[0]['permissions$c-2-0'].checked
-
-    role2.allows_backoffice_access = False
-    role2.store()
-    resp = app.get('/backoffice/settings/admin-permissions')
-    assert resp.forms[0]['permissions$c-0-0'].checked
-    assert not resp.forms[0]['permissions$c-1-0'].checked
-    assert resp.forms[0]['permissions$c-2-0'].checked
-
-    resp.forms[0]['permissions$c-0-0'].checked = False
-    resp.forms[0]['permissions$c-1-0'].checked = True
-    resp = resp.forms[0].submit()
-    assert Role.get(role1.id).allows_backoffice_access is False
-    assert Role.get(role2.id).allows_backoffice_access is True
-
-    # give some roles access to the forms workshop (2nd checkbox) and to the
-    # workflows workshop (4th)
-    resp = app.get('/backoffice/settings/admin-permissions')
-    resp.forms[0]['permissions$c-1-1'].checked = True
-    resp.forms[0]['permissions$c-2-1'].checked = True
-    resp.forms[0]['permissions$c-2-3'].checked = True
-    resp = resp.forms[0].submit()
-    pub.reload_cfg()
-    assert set(pub.cfg['admin-permissions']['forms']) == set([role2.id, role3.id])
-    assert set(pub.cfg['admin-permissions']['workflows']) == set([role3.id])
-
-    # remove accesses
-    resp = app.get('/backoffice/settings/admin-permissions')
-    resp.forms[0]['permissions$c-1-1'].checked = False
-    resp.forms[0]['permissions$c-2-1'].checked = False
-    resp.forms[0]['permissions$c-2-3'].checked = False
-    resp = resp.forms[0].submit()
-    pub.reload_cfg()
-    assert pub.cfg['admin-permissions']['forms'] == []
-    assert pub.cfg['admin-permissions']['workflows'] == []
-
-
-def test_settings_theme_preview(pub):
-    create_superuser(pub)
-
-    FormDef.wipe()
-    formdef = FormDef()
-    formdef.name = 'form title'
-    formdef.fields = []
-    formdef.store()
-
-    app = login(get_app(pub))
-    assert not 'alto/wcs.css' in app.get('/').text
-    resp = app.get('/backoffice/settings/themes')
-    assert resp.form['theme'].value in ('default', 'django')
-
-    # visit theme preview
-    resp = resp.click(href='theme_preview/alto/')
-    assert 'alto/wcs.css' in resp.text
-
-    # get into a form, making sure we are kept in theme preview
-    resp = resp.click('form title')
-    assert 'alto/wcs.css' in resp.text
-
-    # verify submits are not allowed
-    resp = resp.form.submit('submit')
-    assert "The theme preview doesn't support this." in resp.text
-
-
-def test_settings_theme_download_upload(pub):
-    create_superuser(pub)
-
-    # download existing theme
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/themes')
-    resp = resp.click('download', index=0)
-    assert resp.headers['content-type'] == 'application/zip'
-    zip_content = BytesIO(resp.body)
-    zipf = zipfile.ZipFile(zip_content, 'a')
-    filelist = zipf.namelist()
-    assert 'alto/icon.png' in filelist
-    assert 'alto/desc.xml' in filelist
-    assert 'alto/template.ezt' in filelist
-    assert 'alto/wcs.css' in filelist
-
-    # modify it
-    zipf.writestr('alto/foobar.txt', 'XXX')
-    zipf.close()
-
-    # upload it
-    resp = app.get('/backoffice/settings/themes')
-    resp = resp.click('Install New Theme')
-    resp.form['file'] = Upload('alto-modified.zip', zip_content.getvalue())
-    resp = resp.form.submit()
-    assert os.path.exists(os.path.join(pub.app_dir, 'themes/alto/foobar.txt'))
-
-    assert app.get('/themes/alto/foobar.txt').text == 'XXX'
-    assert 'Directory listing denied' in app.get('/themes/alto/', status=200).text
-
-    assert app.get('/themes/alto/plop', status=404)
-    assert app.get('/themes/alto/../', status=404)
-    assert app.get('/themes/xxx/../', status=404)
-
-
-def test_postgresql_settings(pub):
-    if not pub.is_using_postgresql():
-        pytest.skip('this requires SQL')
-        return
-
-    create_superuser(pub)
-
-    database = pub.cfg['postgresql']['database']
-    assert pub.cfg['postgresql'].get('port') == None
-
-    app = login(get_app(pub))
-    resp = app.get('/backoffice/settings/postgresql')
-    assert resp.form['database'].value == database
-    assert resp.form['port'].value == ''
-    resp = resp.form.submit()
-    assert pub.cfg['postgresql']['port'] == None
-
-    pub.cfg['postgresql']['port'] = 5432
-    pub.write_cfg()
-    resp = app.get('/backoffice/settings/postgresql')
-    assert resp.form['port'].value == '5432'
-    resp = resp.form.submit()
-    assert pub.cfg['postgresql']['port'] == 5432
-
-    resp = app.get('/backoffice/settings/postgresql')
-    resp.form['port'] = ''
-    resp = resp.form.submit()
-    assert pub.cfg['postgresql']['port'] == None
-
-    pub.cfg['postgresql']['port'] = '5432' # from an old convert-to-sql (before #10170)
-    pub.write_cfg()
-    resp = app.get('/backoffice/settings/postgresql')
-    assert resp.form['port'].value == '5432'
-    resp = resp.form.submit()
-    assert pub.cfg['postgresql']['port'] == 5432
-
-
 def test_studio_home(pub):
     create_superuser(pub)
     app = login(get_app(pub))
diff --git a/tests/admin_pages/test_role.py b/tests/admin_pages/test_role.py
new file mode 100644
index 00000000..28220366
--- /dev/null
+++ b/tests/admin_pages/test_role.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wcs.qommon.http_request import HTTPRequest
+from wcs.roles import Role
+from wcs.formdef import FormDef
+
+from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
+from .test_all import create_superuser
+
+
+def pytest_generate_tests(metafunc):
+    if 'pub' in metafunc.fixturenames:
+        metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates'], indirect=True)
+
+
+@pytest.fixture
+def pub(request):
+    pub = create_temporary_pub(
+            sql_mode=bool('sql' in request.param),
+            templates_mode=bool('templates' in request.param)
+            )
+
+    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
+    pub.set_app_dir(req)
+    pub.cfg['identification'] = {'methods': ['password']}
+    pub.cfg['language'] = {'language': 'en'}
+    pub.write_cfg()
+
+    return pub
+
+
+def teardown_module(module):
+    clean_temporary_pub()
+
+
+def test_roles(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+    app.get('/backoffice/roles/')
+
+
+def test_roles_new(pub):
+    create_superuser(pub)
+    Role.wipe()
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/roles/')
+    resp = resp.click('New Role')
+    resp.forms[0]['name'] = 'a new role'
+    resp.forms[0]['details'] = 'bla bla bla'
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/roles/'
+    resp = resp.follow()
+    assert 'a new role' in resp.text
+    resp = resp.click('a new role')
+    assert '<h2>a new role' in resp.text
+
+    assert Role.get(1).name == 'a new role'
+    assert Role.get(1).details == 'bla bla bla'
+
+
+def test_roles_edit(pub):
+    create_superuser(pub)
+    Role.wipe()
+    role = Role(name='foobar')
+    role.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/roles/1/')
+    assert 'Holders of this role are granted access to the backoffice' in resp.text
+
+    resp = resp.click(href='edit')
+    assert resp.forms[0]['name'].value == 'foobar'
+    resp.forms[0]['name'] = 'baz'
+    resp.forms[0]['details'] = 'bla bla bla'
+    resp.forms[0]['emails_to_members'].checked = True
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/roles/1/'
+    resp = resp.follow()
+    assert '<h2>baz' in resp.text
+    assert 'Holders of this role will receive all emails adressed to the role.' in resp.text
+
+    assert Role.get(1).details == 'bla bla bla'
+    assert Role.get(1).emails_to_members is True
+
+
+def test_roles_matching_formdefs(pub):
+    create_superuser(pub)
+    Role.wipe()
+    role = Role(name='foo')
+    role.store()
+
+    FormDef.wipe()
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/roles/1/')
+    assert 'form bar' not in resp.text
+
+    formdef = FormDef()
+    formdef.name = 'form bar'
+    formdef.roles = [role.id]
+    formdef.fields = []
+    formdef.store()
+
+    resp = app.get('/backoffice/roles/1/')
+    assert 'form bar' in resp.text
+    assert 'form baz' not in resp.text
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'form baz'
+    formdef.fields = []
+    formdef.workflow_roles = {'_receiver': role.id}
+    formdef.store()
+
+    resp = app.get('/backoffice/roles/1/')
+    assert 'form baz' in resp.text
+    assert 'form bar' not in resp.text
+
+
+def test_roles_delete(pub):
+    create_superuser(pub)
+    Role.wipe()
+    role = Role(name='foobar')
+    role.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/roles/1/')
+
+    resp = resp.click(href='delete')
+    resp = resp.forms[0].submit()
+    assert resp.location == 'http://example.net/backoffice/roles/'
+    resp = resp.follow()
+    assert Role.count() == 0
diff --git a/tests/admin_pages/test_settings.py b/tests/admin_pages/test_settings.py
new file mode 100644
index 00000000..7643ed63
--- /dev/null
+++ b/tests/admin_pages/test_settings.py
@@ -0,0 +1,868 @@
+# -*- coding: utf-8 -*-
+
+import logging
+import os
+import zipfile
+
+try:
+    import lasso
+except ImportError:
+    lasso = None
+
+import pytest
+from webtest import Upload
+import mock
+
+from django.utils.six import BytesIO
+from django.utils.six.moves.urllib import parse as urlparse
+
+from quixote.http_request import Upload as QuixoteUpload
+
+from wcs.qommon.form import UploadedFile
+from wcs.qommon.http_request import HTTPRequest
+from wcs.qommon.template import get_current_theme
+from wcs.api_access import ApiAccess
+from wcs.categories import Category, CardDefCategory
+from wcs.data_sources import NamedDataSource
+from wcs.wscalls import NamedWsCall
+from wcs.roles import Role
+from wcs.workflows import Workflow, CommentableWorkflowStatusItem
+from wcs.wf.export_to_model import ExportToModel
+from wcs.formdef import FormDef
+from wcs.carddef import CardDef
+from wcs import fields
+
+from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
+from .test_all import create_superuser, create_role
+
+
+def pytest_generate_tests(metafunc):
+    if 'pub' in metafunc.fixturenames:
+        metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates'], indirect=True)
+
+
+@pytest.fixture
+def pub(request):
+    pub = create_temporary_pub(
+            sql_mode=bool('sql' in request.param),
+            templates_mode=bool('templates' in request.param)
+            )
+
+    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
+    pub.set_app_dir(req)
+    pub.cfg['identification'] = {'methods': ['password']}
+    pub.cfg['language'] = {'language': 'en'}
+    pub.write_cfg()
+
+    return pub
+
+
+def teardown_module(module):
+    clean_temporary_pub()
+
+
+def test_settings(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+    app.get('/backoffice/settings/')
+
+    app.get('/backoffice/settings/debug_options')
+    app.get('/backoffice/settings/language')
+    app.get('/backoffice/settings/import')
+    app.get('/backoffice/settings/export')
+    app.get('/backoffice/settings/identification')
+    app.get('/backoffice/settings/sitename')
+    app.get('/backoffice/settings/sms')
+    app.get('/backoffice/settings/session')
+    app.get('/backoffice/settings/admin-permissions')
+
+
+def test_settings_disabled_screens(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/')
+    assert 'Identification' in resp.text
+    assert 'Theme' in resp.text
+
+    if not pub.site_options.has_section('options'):
+        pub.site_options.add_section('options')
+    pub.site_options.set('options', 'settings-disabled-screens', 'identification, theme')
+    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
+        pub.site_options.write(fd)
+    resp = app.get('/backoffice/settings/')
+    assert 'Identification' not in resp.text
+    assert 'Theme' not in resp.text
+
+
+def test_settings_export_import(pub):
+    def wipe():
+        FormDef.wipe()
+        CardDef.wipe()
+        Workflow.wipe()
+        Role.wipe()
+        Category.wipe()
+        CardDefCategory.wipe()
+        NamedDataSource.wipe()
+        NamedWsCall.wipe()
+        ApiAccess.wipe()
+
+    wipe()
+    create_superuser(pub)
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/export')
+    resp = resp.form.submit('cancel')
+    resp = app.get('/backoffice/settings/export')
+    resp = resp.form.submit('submit')
+    assert resp.location.startswith('http://example.net/backoffice/settings/export?job=')
+    job_id = urlparse.parse_qs(urlparse.urlparse(resp.location).query)['job'][0]
+    resp = resp.follow()
+    assert 'completed' in resp.text
+    resp = resp.click('Download Export')
+    zip_content = BytesIO(resp.body)
+    zipf = zipfile.ZipFile(zip_content, 'a')
+    filelist = zipf.namelist()
+    assert len(filelist) == 0
+
+    # check afterjob ajax call
+    status_resp = app.get('/afterjobs/' + job_id)
+    assert status_resp.text == 'completed|completed'
+
+    formdef = FormDef()
+    formdef.name = 'foo'
+    formdef.store()
+    carddef = CardDef()
+    carddef.name = 'bar'
+    carddef.store()
+    Category(name='baz').store()
+    CardDefCategory(name='foobar').store()
+    Role(name='qux').store()
+    NamedDataSource(name='quux').store()
+    NamedWsCall(name='corge').store()
+
+    wf = Workflow(name='bar')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.label = 'test'
+    upload = QuixoteUpload('/foo/bar', content_type='application/vnd.oasis.opendocument.text')
+    file_content = b'''PK\x03\x04\x14\x00\x00\x08\x00\x00\'l\x8eG^\xc62\x0c\'\x00'''
+    upload.fp = BytesIO()
+    upload.fp.write(file_content)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile('models', 'export_to_model-1.upload', upload)
+    st1.items.append(export_to)
+    export_to.parent = st1
+    wf.store()
+
+    api_access = ApiAccess()
+    api_access.name = 'Jhon'
+    api_access.api_identifier = 'jhon'
+    api_access.api_key = '1234'
+    api_access.store()
+
+    resp = app.get('/backoffice/settings/export')
+    resp = resp.form.submit('submit')
+    assert resp.location.startswith('http://example.net/backoffice/settings/export?job=')
+    job_id = urlparse.parse_qs(urlparse.urlparse(resp.location).query)['job'][0]
+    resp = resp.follow()
+    resp = resp.click('Download Export')
+    zip_content = BytesIO(resp.body)
+    zipf = zipfile.ZipFile(zip_content, 'a')
+    filelist = zipf.namelist()
+    assert 'formdefs/1' not in filelist
+    assert 'formdefs_xml/1' in filelist
+    assert 'carddefs/1' not in filelist
+    assert 'carddefs_xml/1' in filelist
+    assert 'workflows/1' not in filelist
+    assert 'workflows_xml/1' in filelist
+    assert 'models/export_to_model-1.upload' not in filelist
+    assert 'roles/1' in filelist
+    assert 'categories/1' in filelist
+    assert 'carddef_categories/1' in filelist
+    assert 'datasources/1' in filelist
+    assert 'wscalls/corge' in filelist
+    assert 'apiaccess/1' in filelist
+    for filename in filelist:
+        assert '.indexes' not in filename
+
+    wipe()
+    assert FormDef.count() == 0
+
+    resp = app.get('/backoffice/settings/import')
+    resp = resp.form.submit('cancel')
+
+    resp = app.get('/backoffice/settings/import')
+    resp.form['file'] = Upload('export.wcs', b'invalid content')
+    resp = resp.form.submit('submit')
+    assert 'Error: Not a valid export file' in resp.text
+
+    resp = app.get('/backoffice/settings/import')
+    resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
+    resp = resp.form.submit('submit')
+    assert 'Imported successfully' in resp.text
+    assert '1 forms' in resp.text
+    assert '1 cards' in resp.text
+    assert FormDef.count() == 1
+    assert FormDef.select()[0].url_name == 'foo'
+    assert CardDef.count() == 1
+    assert CardDef.select()[0].url_name == 'bar'
+    assert ApiAccess.count() == 1
+
+    # check roles are found by name
+    wipe()
+    role = Role(name='qux')
+    role.store()
+
+    workflow = Workflow(name='Workflow One')
+    st1 = workflow.add_status(name='st1')
+    commentable = CommentableWorkflowStatusItem()
+    commentable.id = '_commentable'
+    commentable.by = [role.id]
+    commentable.label = 'foobar'
+    st1.items.append(commentable)
+    commentable.parent = st1
+    workflow.store()
+
+    formdef = FormDef()
+    formdef.name = 'foo'
+    formdef.workflow_id = workflow.id
+    formdef.roles = [role.id]
+    formdef.backoffice_submission_roles = [role.id]
+    formdef.workflow_roles = {'_receiver': role.id}
+    formdef.fields = [fields.StringField(
+        id='1', type='string',
+        data_source={'type': 'carddef:unknown'})]
+    formdef.store()
+
+    resp = app.get('/backoffice/settings/export')
+    resp.form['formdefs'] = True
+    resp.form['workflows'] = True
+    resp.form['roles'] = False
+    resp.form['categories'] = False
+    resp.form['datasources'] = False
+    resp.form['wscalls'] = False
+    resp = resp.form.submit('submit')
+    assert resp.location.startswith('http://example.net/backoffice/settings/export?job=')
+    job_id = urlparse.parse_qs(urlparse.urlparse(resp.location).query)['job'][0]
+    resp = resp.follow()
+    resp = resp.click('Download Export')
+    zip_content = BytesIO(resp.body)
+    zipf = zipfile.ZipFile(zip_content, 'a')
+    filelist = zipf.namelist()
+    assert 'formdefs_xml/%s' % formdef.id in filelist
+    assert 'workflows_xml/%s' % workflow.id in filelist
+    assert 'roles/%s' % role.id not in filelist
+
+    FormDef.wipe()
+    Workflow.wipe()
+    Role.wipe()
+
+    # create role beforehand, it should be matched by name
+    role = Role(name='qux')
+    role.id = '012345'
+    role.store()
+
+    resp = app.get('/backoffice/settings/import')
+    resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
+    resp = resp.form.submit('submit')
+    assert FormDef.select()[0].roles == ['012345']
+    assert FormDef.select()[0].backoffice_submission_roles == ['012345']
+    assert FormDef.select()[0].workflow_roles == {'_receiver': '012345'}
+    assert len(FormDef.select()[0].fields) == 1
+    assert Workflow.select()[0].possible_status[0].items[0].by == ['012345']
+
+    # do not export roles when managed by idp
+    pub.cfg['sp'] = {'idp-manage-roles': True}
+    pub.write_cfg()
+    resp = app.get('/backoffice/settings/export')
+    resp = resp.form.submit('submit')
+    assert resp.location.startswith('http://example.net/backoffice/settings/export?job=')
+    job_id = urlparse.parse_qs(urlparse.urlparse(resp.location).query)['job'][0]
+    resp = resp.follow()
+    resp = resp.click('Download Export')
+    zip_content = BytesIO(resp.body)
+    zipf = zipfile.ZipFile(zip_content, 'a')
+    filelist = zipf.namelist()
+    assert len([x for x in filelist if 'roles/' in x]) == 0
+
+    # check an error is displayed if such an import is then used and roles are
+    # missing.
+    FormDef.wipe()
+    Workflow.wipe()
+    Role.wipe()
+    resp = app.get('/backoffice/settings/import')
+    resp.form['file'] = Upload('export.wcs', zip_content.getvalue())
+    resp = resp.form.submit('submit')
+    assert 'Unknown referenced role (qux)' in resp
+
+
+def test_settings_themes(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+
+    # create mock theme
+    os.mkdir(os.path.join(pub.app_dir, 'themes'))
+    os.mkdir(os.path.join(pub.app_dir, 'themes', 'test'))
+    fd = open(os.path.join(pub.app_dir, 'themes', 'test', 'desc.xml'), 'w')
+    fd.write('<?xml version="1.0"?>'
+             '<theme name="test" version="1.0">'
+             '  <label>Test Theme</label>'
+             '</theme>')
+    fd.close()
+
+    resp = app.get('/backoffice/settings/themes')
+    assert 'biglist themes' in resp.text
+    assert 'Test Theme (1.0)' in resp.text
+
+    # just for the kick, there's no support for uploading file in webtest 1.3
+    resp = app.get('/backoffice/settings/themes')
+    resp.click('Install New Theme')
+
+    # select the theme
+    resp = app.get('/backoffice/settings/themes')
+    resp.forms[0]['theme'].value = 'test'
+    resp = resp.forms[0].submit()
+    assert resp.location == 'http://example.net/backoffice/settings/'
+
+    resp = app.get('/backoffice/settings/themes')
+    assert 'checked' in resp.text
+    assert get_current_theme()['name'] == 'test'
+
+
+def test_settings_template(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/template')
+
+    # change template
+    orig_value = resp.forms[0]['template'].value
+    assert 'foobar' not in orig_value
+    resp.forms[0]['template'] = orig_value + '<!-- foobar -->'
+    resp = resp.forms[0].submit('submit')
+
+    # restore default template
+    resp = app.get('/backoffice/settings/template')
+    assert 'foobar' in resp.forms[0]['template'].value
+    resp = resp.forms[0].submit('restore-default')
+
+    # check
+    resp = app.get('/backoffice/settings/template')
+    assert resp.forms[0]['template'].value == orig_value
+
+
+def test_settings_user(pub):
+    user = create_superuser(pub)
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/users').follow().follow()
+
+    # add a field
+    resp.forms[2]['label'] = 'foobar'
+    resp = resp.forms[2].submit()
+    assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
+    resp = resp.follow()
+    assert b'foobar' in pub.cfg['users']['formdef']
+    assert 'foobar' in resp.text
+
+    # set field as email
+    resp.forms['mapping']['field_email'] = '1'
+    resp = resp.forms['mapping'].submit()
+    assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
+    resp = resp.follow()
+    assert pub.cfg['users']['field_email'] == '1'
+
+    # and unset it
+    resp.forms['mapping']['field_email'] = ''
+    resp = resp.forms['mapping'].submit()
+    assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
+    resp = resp.follow()
+    assert pub.cfg['users']['field_email'] is None
+
+    # add a comment field
+    resp.forms[2]['label'] = 'barfoo'
+    resp.forms[2]['type'] = 'comment'
+    resp = resp.forms[2].submit()
+    assert resp.location == 'http://example.net/backoffice/settings/users/fields/'
+    resp = resp.follow()
+    assert b'barfoo' in pub.cfg['users']['formdef']
+    assert 'barfoo' in resp.text
+
+    # check fields are present in edit form
+    resp = app.get('/backoffice/users/%s/edit' % user.id)
+    assert 'barfoo' in resp.text
+    assert 'f1' in resp.form.fields
+    assert 'email' in resp.form.fields
+
+    # check the email field is not displayed if it's overridden by a custom
+    # field.
+    pub.cfg['users']['field_email'] = '1'
+    pub.write_cfg()
+    resp = app.get('/backoffice/users/%s/edit' % user.id)
+    assert 'f1' in resp.form.fields
+    assert 'email' not in resp.form.fields
+
+    # set a sidebar template
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/users').follow().follow()
+    resp.forms['template']['sidebar_template'] = 'hello {{ form_user_display_name }}'
+    resp = resp.forms['template'].submit().follow()
+    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
+    resp.forms['template']['sidebar_template'] = '{% if True %}'
+    resp = resp.forms['template'].submit().follow()
+    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
+    assert 'syntax error in Django template' in resp
+
+    # disable users screen
+    if not pub.site_options.has_section('options'):
+        pub.site_options.add_section('options')
+    pub.site_options.set('options', 'settings-disabled-screens', 'users')
+    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
+        pub.site_options.write(fd)
+
+    resp = app.get('/backoffice/settings/')
+    resp = resp.click('Users', href='user-template')
+    resp.forms['template']['sidebar_template'] = '{% if True %}'
+    resp = resp.forms['template'].submit()
+    assert 'syntax error in Django template' in resp
+    resp.forms['template']['sidebar_template'] = 'hello {{ form_user_display_name }}'
+    resp = resp.forms['template'].submit()
+    assert pub.cfg['users']['sidebar_template'] == 'hello {{ form_user_display_name }}'
+
+    # restore config
+    pub.cfg['users']['field_email'] = None
+    pub.write_cfg()
+
+
+def test_settings_emails(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+
+    pub.cfg['debug'] = {'mail_redirection': 'foo@example.net'}
+    pub.write_cfg()
+    resp = app.get('/backoffice/settings/emails/')
+    resp = resp.click('General Options')
+    assert 'Warning: all emails are sent to &lt;foo@example.net&gt;' in resp.text
+    resp.form['from'] = 'test@localhost'
+    resp = resp.form.submit('submit')
+    pub.reload_cfg()
+    assert pub.cfg['emails']['from'] == 'test@localhost'
+    assert pub.cfg['emails']['well_known_domains']
+    assert pub.cfg['emails']['valid_known_domains']
+
+    pub.cfg['debug'] = {}
+    pub.write_cfg()
+    resp = app.get('/backoffice/settings/emails/')
+    resp = resp.click('General Options')
+    assert 'Warning: all emails are sent to &lt;foo@example.net&gt;' not in resp.text
+
+    resp = app.get('/backoffice/settings/emails/')
+    resp = resp.click('Approval of new account')
+    resp.forms[0]['email-new-account-approved_subject'] = 'bla'
+    resp.forms[0]['email-new-account-approved'] = 'bla bla bla'
+    resp = resp.forms[0].submit()
+    assert pub.cfg['emails']['email-new-account-approved_subject'] == 'bla'
+    assert pub.cfg['emails']['email-new-account-approved'] == 'bla bla bla'
+
+    # reset to default value
+    resp = app.get('/backoffice/settings/emails/')
+    resp = resp.click('Approval of new account')
+    resp.forms[0]['email-new-account-approved_subject'] = 'Your account has been approved'
+    resp = resp.forms[0].submit()
+    assert pub.cfg['emails']['email-new-account-approved_subject'] is None
+
+
+def test_settings_texts(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+
+    resp = app.get('/backoffice/settings/texts/')
+    resp = resp.click('Text on top of the login page')
+    resp.forms[0]['text-top-of-login'] = 'Hello world'
+    resp = resp.forms[0].submit()
+    assert resp.location == 'http://example.net/backoffice/settings/texts/'
+    assert pub.cfg['texts']['text-top-of-login'] == 'Hello world'
+
+    resp = app.get('/backoffice/settings/texts/')
+    resp = resp.click('Text on top of the login page')
+    resp = resp.forms[0].submit('restore-default')
+    assert resp.location == 'http://example.net/backoffice/settings/texts/'
+    assert pub.cfg['texts']['text-top-of-login'] is None
+
+
+@pytest.mark.skipif('lasso is None')
+def test_settings_auth(pub):
+    pub.user_class.wipe()  # makes sure there are no users
+    pub.cfg['identification'] = {}
+    pub.write_cfg()
+    app = get_app(pub)
+
+    resp = app.get('/backoffice/settings/')
+    assert 'identification/password/' not in resp.text
+    assert 'identification/idp/' not in resp.text
+
+    resp = resp.click('Identification')
+    assert resp.forms[0]['methods$elementidp'].checked is False
+    assert resp.forms[0]['methods$elementpassword'].checked is False
+    resp.forms[0]['methods$elementidp'].checked = True
+    resp = resp.forms[0].submit()
+    resp = resp.follow()
+    assert 'identification/idp/' in resp.text
+    assert pub.cfg['identification']['methods'] == ['idp']
+
+    resp = resp.click('Identification')
+    assert resp.forms[0]['methods$elementidp'].checked is True
+    assert resp.forms[0]['methods$elementpassword'].checked is False
+    resp.forms[0]['methods$elementidp'].checked = False
+    resp.forms[0]['methods$elementpassword'].checked = True
+    resp = resp.forms[0].submit()
+    resp = resp.follow()
+    assert 'identification/password/' in resp.text
+    assert pub.cfg['identification']['methods'] == ['password']
+
+
+@pytest.mark.skipif('lasso is None')
+def test_settings_idp(pub):
+    # create admin session
+    create_superuser(pub)
+    app = login(get_app(pub))
+
+    pub.cfg['identification'] = {'methods': ['idp']}
+    pub.write_cfg()
+    app.get('/saml/metadata', status=404)
+    resp = app.get('/backoffice/settings/')
+    resp = resp.click(href='identification/idp/')
+    resp = resp.click('Service Provider')
+    resp = resp.form.submit('generate_rsa').follow()
+    resp = resp.form.submit('submit')
+    resp = resp.follow()
+
+    resp2 = resp.click('Identities')
+    resp2 = resp2.form.submit('cancel').follow()
+    resp2 = resp.click('Identities')
+    resp2 = resp2.form.submit('submit')
+
+    resp_metadata = app.get('/saml/metadata', status=200)
+    assert resp_metadata.text.startswith('<?xml')
+    resp2 = resp.click('Identity Providers')
+    resp2 = resp2.click('New')
+    idp_metadata_filename = os.path.join(os.path.dirname(__file__), '..', 'idp_metadata.xml')
+    resp2.form['metadata'] = Upload('idp_metadata.xml', open(idp_metadata_filename, 'rb').read())
+    resp2 = resp2.form.submit('submit')
+
+    resp = resp.click('Identity Providers')
+    assert 'http://authentic.example.net/' in resp.text
+    resp2 = resp.click(href='http-authentic.example.net-idp-saml2-metadata/', index=0)
+    assert 'ns0:EntityDescriptor' in resp2.text
+    resp = resp.click(href='http-authentic.example.net-idp-saml2-metadata/edit')
+    resp = resp.forms[0].submit('submit')
+    resp = resp.follow()
+
+    # test that login initiates a SSO
+    login_resp = app.get('/login/', status=302)
+    assert login_resp.location.startswith('http://authentic.example.net/idp/saml2/sso?SAMLRequest')
+
+    resp = resp.click(href='/backoffice/settings/identification/idp/idp/', index=0)  # breadcrumb
+    resp = resp.click(href='http-authentic.example.net-idp-saml2-metadata/delete')
+    resp = resp.forms[0].submit()  # confirm delete
+    assert len(pub.cfg['idp']) == 0
+
+    with mock.patch('wcs.qommon.misc.urlopen') as urlopen:
+        idp_metadata_filename = os.path.join(os.path.dirname(__file__), '..', 'idp_metadata.xml')
+        urlopen.side_effect = lambda *args: open(idp_metadata_filename, 'rb')
+        resp = app.get('/backoffice/settings/identification/idp/idp/')
+        resp = resp.click('Create new from remote URL')
+        resp.form['metadata_url'] = 'http://authentic.example.net/idp/saml2/metadata'
+        resp = resp.form.submit('submit')
+        resp = resp.follow()
+        assert 'http://authentic.example.net/idp/saml2/metadata' in resp.text
+        assert urlopen.call_count == 1
+        resp = resp.click('View')
+        resp = resp.click('Update from remote URL')
+        assert urlopen.call_count == 2
+
+
+def test_settings_auth_password(pub):
+    Role.wipe()
+
+    pub.user_class.wipe()  # makes sure there are no users
+    pub.cfg['identification'] = {'methods': ['password']}
+    assert pub.cfg['identification']['methods'] == ['password']
+    pub.write_cfg()
+    app = get_app(pub)
+
+    resp = app.get('/backoffice/settings/identification/password/')
+    resp = resp.click('Identities')
+    resp = resp.forms[0].submit()
+
+    resp = app.get('/backoffice/settings/identification/password/')
+    resp = resp.click('Passwords')
+    resp = resp.forms[0].submit()
+
+
+def test_settings_filetypes(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+
+    resp = app.get('/backoffice/settings/filetypes/')
+    assert 'There are no file type defined at the moment.' in resp.text
+    resp.forms[0]['label'] = 'Text files'
+    resp.forms[0]['mimetypes'] = '.odt'
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/settings/filetypes/'
+    resp = resp.follow()
+    assert pub.cfg['filetypes'][1]['label'] == 'Text files'
+
+    resp = resp.click('Text files')
+    assert resp.forms[0]['mimetypes'].value == 'application/vnd.oasis.opendocument.text'
+    resp.forms[0]['mimetypes'] = 'application/vnd.oasis.opendocument.text, .doc, .docx, .pdf'
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/settings/filetypes/'
+    resp = resp.follow()
+    assert 'application/msword (.' in resp.text
+    assert 'application/pdf' in pub.cfg['filetypes'][1]['mimetypes']
+
+    resp.forms[0]['label'] = 'HTML files'
+    resp.forms[0]['mimetypes'] = '.html'
+    resp = resp.forms[0].submit('submit')
+    resp = resp.follow()
+    resp = resp.click('HTML files')  # go to form
+    resp = resp.forms[0].submit('cancel')  # and cancel
+    assert resp.location == 'http://example.net/backoffice/settings/filetypes/'
+    resp = resp.follow()
+    assert 'HTML files' in resp.text
+
+    resp = resp.click('HTML files')  # go to form
+    resp = resp.forms[0].submit('delete')  # and delete
+    assert resp.location == 'http://example.net/backoffice/settings/filetypes/'
+    resp = resp.follow()
+    assert 'HTML files' not in resp.text
+
+
+def test_settings_filetypes_update(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+
+    pub.cfg['filetypes'] = {
+            1: {'mimetypes': ['application/pdf', 'application/msword'],
+                'label': 'Text files'}
+            }
+    pub.write_cfg()
+    resp = app.get('/backoffice/settings/filetypes/')
+    assert 'Text files' in resp.text
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'form title'
+    formdef.fields = [fields.FileField(
+            id='1', label='1st field', type='file',
+            document_type={
+                'id': 1,
+                'mimetypes': ['application/pdf', 'application/msword'],
+                'label': 'Text files',
+            })]
+    formdef.store()
+    assert FormDef.get(formdef.id).fields[0].document_type == {
+                'id': 1,
+                'mimetypes': ['application/pdf', 'application/msword'],
+                'label': 'Text files',
+            }
+
+    resp = resp.click('Text files')
+    resp.forms[0]['mimetypes'] = 'application/vnd.oasis.opendocument.text, .doc, .docx, .pdf'
+    resp = resp.forms[0].submit('submit')
+    assert 'application/pdf' in pub.cfg['filetypes'][1]['mimetypes']
+    assert FormDef.get(formdef.id).fields[0].document_type == {
+                'id': 1,
+                'mimetypes': ['application/vnd.oasis.opendocument.text',
+                              'application/msword',
+                              'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+                              'application/pdf'],
+                'label': 'Text files',
+            }
+
+
+def test_settings_logs(pub):
+    # reset logging state
+    logging.shutdown()
+    if os.path.exists(os.path.join(pub.app_dir, 'wcs.log')):
+        os.unlink(os.path.join(pub.app_dir, 'wcs.log'))
+
+    create_superuser(pub)
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/')
+    assert 'Logs' in resp.text
+    resp = resp.click('Logs')
+    assert '<td class="message">login</td>' in resp.text
+
+    resp = app.get('/backoffice/settings/debug_options')
+    assert resp.form['logger'].checked is True
+    resp.form['logger'].checked = False
+    resp = resp.form.submit()
+    resp = resp.follow()
+    assert 'Logs' not in resp.text
+
+
+def test_settings_geolocation(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/')
+    resp = resp.click('Geolocation')
+    resp.form['default-position$latlng'].value = '1.234;-1.234'
+    resp = resp.form.submit('cancel').follow()
+    resp = resp.click('Geolocation')
+    assert 'value="1.234;-1.234' not in resp.text
+    resp.form['default-position$latlng'].value = '1.234;-1.234'
+    resp = resp.form.submit().follow()
+    resp = resp.click('Geolocation')
+    assert 'value="1.234;-1.234' in resp.text
+    pub.reload_cfg()
+    assert pub.cfg['misc']['default-position'] == '1.234;-1.234'
+
+
+def test_settings_permissions(pub):
+    create_superuser(pub)
+    role1 = create_role()
+    role1.name = 'foobar1'
+    role1.store()
+    role2 = Role(name='foobar2')
+    role2.store()
+    role3 = Role(name='foobar3')
+    role3.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/admin-permissions')
+    # assert all first checkboxes are checked
+    assert resp.forms[0]['permissions$c-0-0'].checked
+    assert resp.forms[0]['permissions$c-1-0'].checked
+    assert resp.forms[0]['permissions$c-2-0'].checked
+
+    role2.allows_backoffice_access = False
+    role2.store()
+    resp = app.get('/backoffice/settings/admin-permissions')
+    assert resp.forms[0]['permissions$c-0-0'].checked
+    assert not resp.forms[0]['permissions$c-1-0'].checked
+    assert resp.forms[0]['permissions$c-2-0'].checked
+
+    resp.forms[0]['permissions$c-0-0'].checked = False
+    resp.forms[0]['permissions$c-1-0'].checked = True
+    resp = resp.forms[0].submit()
+    assert Role.get(role1.id).allows_backoffice_access is False
+    assert Role.get(role2.id).allows_backoffice_access is True
+
+    # give some roles access to the forms workshop (2nd checkbox) and to the
+    # workflows workshop (4th)
+    resp = app.get('/backoffice/settings/admin-permissions')
+    resp.forms[0]['permissions$c-1-1'].checked = True
+    resp.forms[0]['permissions$c-2-1'].checked = True
+    resp.forms[0]['permissions$c-2-3'].checked = True
+    resp = resp.forms[0].submit()
+    pub.reload_cfg()
+    assert set(pub.cfg['admin-permissions']['forms']) == set([role2.id, role3.id])
+    assert set(pub.cfg['admin-permissions']['workflows']) == set([role3.id])
+
+    # remove accesses
+    resp = app.get('/backoffice/settings/admin-permissions')
+    resp.forms[0]['permissions$c-1-1'].checked = False
+    resp.forms[0]['permissions$c-2-1'].checked = False
+    resp.forms[0]['permissions$c-2-3'].checked = False
+    resp = resp.forms[0].submit()
+    pub.reload_cfg()
+    assert pub.cfg['admin-permissions']['forms'] == []
+    assert pub.cfg['admin-permissions']['workflows'] == []
+
+
+def test_settings_theme_preview(pub):
+    create_superuser(pub)
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'form title'
+    formdef.fields = []
+    formdef.store()
+
+    app = login(get_app(pub))
+    assert 'alto/wcs.css' not in app.get('/').text
+    resp = app.get('/backoffice/settings/themes')
+    assert resp.form['theme'].value in ('default', 'django')
+
+    # visit theme preview
+    resp = resp.click(href='theme_preview/alto/')
+    assert 'alto/wcs.css' in resp.text
+
+    # get into a form, making sure we are kept in theme preview
+    resp = resp.click('form title')
+    assert 'alto/wcs.css' in resp.text
+
+    # verify submits are not allowed
+    resp = resp.form.submit('submit')
+    assert "The theme preview doesn't support this." in resp.text
+
+
+def test_settings_theme_download_upload(pub):
+    create_superuser(pub)
+
+    # download existing theme
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/themes')
+    resp = resp.click('download', index=0)
+    assert resp.headers['content-type'] == 'application/zip'
+    zip_content = BytesIO(resp.body)
+    zipf = zipfile.ZipFile(zip_content, 'a')
+    filelist = zipf.namelist()
+    assert 'alto/icon.png' in filelist
+    assert 'alto/desc.xml' in filelist
+    assert 'alto/template.ezt' in filelist
+    assert 'alto/wcs.css' in filelist
+
+    # modify it
+    zipf.writestr('alto/foobar.txt', 'XXX')
+    zipf.close()
+
+    # upload it
+    resp = app.get('/backoffice/settings/themes')
+    resp = resp.click('Install New Theme')
+    resp.form['file'] = Upload('alto-modified.zip', zip_content.getvalue())
+    resp = resp.form.submit()
+    assert os.path.exists(os.path.join(pub.app_dir, 'themes/alto/foobar.txt'))
+
+    assert app.get('/themes/alto/foobar.txt').text == 'XXX'
+    assert 'Directory listing denied' in app.get('/themes/alto/', status=200).text
+
+    assert app.get('/themes/alto/plop', status=404)
+    assert app.get('/themes/alto/../', status=404)
+    assert app.get('/themes/xxx/../', status=404)
+
+
+def test_postgresql_settings(pub):
+    if not pub.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
+    create_superuser(pub)
+
+    database = pub.cfg['postgresql']['database']
+    assert pub.cfg['postgresql'].get('port') is None
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/settings/postgresql')
+    assert resp.form['database'].value == database
+    assert resp.form['port'].value == ''
+    resp = resp.form.submit()
+    assert pub.cfg['postgresql']['port'] is None
+
+    pub.cfg['postgresql']['port'] = 5432
+    pub.write_cfg()
+    resp = app.get('/backoffice/settings/postgresql')
+    assert resp.form['port'].value == '5432'
+    resp = resp.form.submit()
+    assert pub.cfg['postgresql']['port'] == 5432
+
+    resp = app.get('/backoffice/settings/postgresql')
+    resp.form['port'] = ''
+    resp = resp.form.submit()
+    assert pub.cfg['postgresql']['port'] is None
+
+    pub.cfg['postgresql']['port'] = '5432'  # from an old convert-to-sql (before #10170)
+    pub.write_cfg()
+    resp = app.get('/backoffice/settings/postgresql')
+    assert resp.form['port'].value == '5432'
+    resp = resp.form.submit()
+    assert pub.cfg['postgresql']['port'] == 5432
diff --git a/tests/admin_pages/test_user.py b/tests/admin_pages/test_user.py
new file mode 100644
index 00000000..a58c3244
--- /dev/null
+++ b/tests/admin_pages/test_user.py
@@ -0,0 +1,344 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wcs.qommon.ident.password_accounts import PasswordAccount
+from wcs.qommon.http_request import HTTPRequest
+from wcs.admin.settings import UserFieldsFormDef
+from wcs import fields
+
+from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
+from .test_all import create_superuser, create_role
+
+
+def pytest_generate_tests(metafunc):
+    if 'pub' in metafunc.fixturenames:
+        metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates'], indirect=True)
+
+
+@pytest.fixture
+def pub(request):
+    pub = create_temporary_pub(
+            sql_mode=bool('sql' in request.param),
+            templates_mode=bool('templates' in request.param)
+            )
+
+    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
+    pub.set_app_dir(req)
+    pub.cfg['identification'] = {'methods': ['password']}
+    pub.cfg['language'] = {'language': 'en'}
+    pub.write_cfg()
+
+    return pub
+
+
+def teardown_module(module):
+    clean_temporary_pub()
+
+
+def test_users(pub):
+    create_superuser(pub)
+    app = login(get_app(pub))
+    app.get('/backoffice/users/')
+
+
+def test_users_new(pub):
+    pub.user_class.wipe()
+    create_superuser(pub)
+    user_count = pub.user_class.count()
+    account_count = PasswordAccount.count()
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/')
+    resp = resp.click('New User')
+    resp.forms[0]['name'] = 'a new user'
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/users/'
+    resp = resp.follow()
+    assert 'a new user' in resp.text
+    resp = resp.click('a new user')
+    assert 'User - a new user' in resp.text
+    assert pub.user_class.count() == user_count + 1
+    assert PasswordAccount.count() == account_count
+
+
+def test_users_new_with_account(pub):
+    pub.user_class.wipe()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    user_count = pub.user_class.count()
+    account_count = PasswordAccount.count()
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/')
+    resp = resp.click('New User')
+    resp.forms[0]['name'] = 'a second user'
+    resp.forms[0]['method_password$username'] = 'second-user'
+    resp.forms[0]['method_password$password'] = 'foobar'
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/users/'
+    resp = resp.follow()
+    assert 'a second user' in resp.text
+    resp = resp.click('a second user')
+    assert 'User - a second user' in resp.text
+    assert pub.user_class.count() == user_count + 1
+    assert PasswordAccount.count() == account_count + 1
+
+
+def test_users_edit(pub):
+    pub.user_class.wipe()
+    create_superuser(pub)
+    user = pub.user_class(name='foo bar')
+    user.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/%s/' % user.id)
+    resp = resp.click(href='edit')
+    resp.forms[0]['is_admin'].checked = True
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
+    resp = resp.follow()
+
+
+def test_users_edit_new_account(pub):
+    pub.user_class.wipe()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    user = pub.user_class(name='foo bar')
+    user.store()
+    account_count = PasswordAccount.count()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/%s/' % user.id)
+    resp = resp.click(href='edit')
+    resp.forms[0]['is_admin'].checked = True
+    resp.forms[0]['method_password$username'] = 'foo'
+    resp.forms[0]['method_password$password'] = 'bar'
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
+    resp = resp.follow()
+
+    assert PasswordAccount.count() == account_count + 1
+
+
+def test_users_edit_edit_account(pub):
+    pub.user_class.wipe()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    user = pub.user_class(name='foo bar')
+    user.store()
+    account = PasswordAccount(id='test')
+    account.user_id = user.id
+    account.store()
+    assert PasswordAccount.has_key('test')
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/%s/' % user.id)
+    resp = resp.click(href='edit')
+    resp.forms[0]['is_admin'].checked = True
+    resp.forms[0]['method_password$username'] = 'foo'  # change username
+    resp.forms[0]['method_password$password'] = 'bar'
+    resp = resp.forms[0].submit('submit')
+    assert resp.location == 'http://example.net/backoffice/users/%s/' % user.id
+    resp = resp.follow()
+
+    # makes sure the old account has been removed
+    assert not PasswordAccount.has_key('test')
+    assert PasswordAccount.has_key('foo')
+    assert PasswordAccount.get('foo').user_id == user.id
+
+
+def test_users_edit_with_managing_idp(pub):
+    create_role()
+    pub.user_class.wipe()
+    pub.cfg['sp'] = {'idp-manage-user-attributes': True}
+    pub.write_cfg()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    user = pub.user_class(name='foo bar')
+    user.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/%s/' % user.id)
+    assert '>Manage Roles<' in resp.text
+    resp = resp.click(href='edit')
+    assert 'email' not in resp.form.fields
+    assert 'roles$add_element' in resp.form.fields
+
+    pub.cfg['sp'] = {'idp-manage-roles': True}
+    pub.write_cfg()
+    resp = app.get('/backoffice/users/%s/' % user.id)
+    assert '>Edit<' in resp.text
+    resp = resp.click(href='edit')
+    assert 'email' in resp.form.fields
+    assert 'roles$add_element' not in resp.form.fields
+
+    pub.cfg['sp'] = {'idp-manage-roles': True, 'idp-manage-user-attributes': True}
+    pub.write_cfg()
+    resp = app.get('/backoffice/users/%s/' % user.id)
+    assert '/edit' not in resp.text
+
+
+def test_users_delete(pub):
+    pub.user_class.wipe()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    user = pub.user_class(name='foo bar')
+    user.store()
+    account = PasswordAccount(id='test')
+    account.user_id = user.id
+    account.store()
+
+    user_count = pub.user_class.count()
+    account_count = PasswordAccount.count()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/%s/' % user.id)
+
+    resp = resp.click(href='delete')
+    resp = resp.forms[0].submit()
+    assert resp.location == 'http://example.net/backoffice/users/'
+    resp = resp.follow()
+
+    assert pub.user_class.count() == user_count - 1
+    assert PasswordAccount.count() == account_count - 1
+
+
+def test_users_view_deleted(pub):
+    pub.user_class.wipe()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    user = pub.user_class(name='foo bar')
+    user.store()
+    account = PasswordAccount(id='test')
+    account.user_id = user.id
+    account.store()
+
+    user.set_deleted()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/%s/' % user.id)
+    assert 'Marked as deleted on' in resp
+
+
+def test_users_pagination(pub):
+    pub.user_class.wipe()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    for i in range(50):
+        user = pub.user_class(name='foo bar %s' % (i+1))
+        user.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/')
+    assert 'foo bar 10' in resp.text
+    assert 'foo bar 30' not in resp.text
+
+    resp = resp.click('Next Page')
+    assert 'foo bar 10' not in resp.text
+    assert 'foo bar 30' in resp.text
+
+    resp = resp.click('Previous Page')
+    assert 'foo bar 10' in resp.text
+    assert 'foo bar 30' not in resp.text
+
+    resp = resp.click('Next Page')
+    resp = resp.click('Next Page')
+    assert 'foo bar 50' in resp.text
+
+
+def test_users_filter(pub):
+    pub.user_class.wipe()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    role = create_role()
+    for i in range(50):
+        user = pub.user_class(name='foo bar %s' % (i+1))
+        user.store()
+
+    for i in range(5):
+        user = pub.user_class(name='baz bar %s' % (i+1))
+        user.roles = [role.id]
+        user.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/')
+    assert 'admin' in resp.text  # superuser
+    assert 'foo bar 10' in resp.text  # simple user
+
+    # uncheck 'None'; unfortunately this doesn't work with webtest 1.3
+    #   resp.forms[0].fields['role'][-1].checked = False
+    #   resp = resp.forms[0].submit()
+    # therefore we fall back on using the URL
+    resp = app.get('/backoffice/users/?offset=0&limit=100&q=&filter=true&role=admin')
+    assert '>Number of filtered users: 1<' in resp.text
+    assert 'user-is-admin' in resp.text  # superuser
+    assert 'foo bar 1' not in resp.text  # simple user
+    assert 'baz bar 1' not in resp.text  # user with role
+
+    resp = app.get('/backoffice/users/?offset=0&limit=100&q=&filter=true&role=1')
+    assert '>Number of filtered users: 5<' in resp.text
+    assert 'user-is-admin' not in resp.text  # superuser
+    assert 'foo bar 10' not in resp.text  # simple user
+    assert 'baz bar 1' in resp.text  # user with role
+
+
+def test_users_search(pub):
+    pub.user_class.wipe()
+    PasswordAccount.wipe()
+    create_superuser(pub)
+    for i in range(20):
+        user = pub.user_class(name='foo %s' % (i+1))
+        user.store()
+    for i in range(10):
+        user = pub.user_class(name='bar %s' % (i+1))
+        user.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/')
+    assert 'foo 10' in resp.text
+
+    resp.forms[0]['q'] = 'bar'
+    resp = resp.forms[0].submit()
+    assert 'foo 10' not in resp.text
+    assert 'bar 10' in resp.text
+    assert 'Number of filtered users: 10' in resp.text
+
+
+def test_users_new_with_custom_formdef(pub):
+    pub.user_class.wipe()
+    formdef = UserFieldsFormDef(pub)
+    formdef.fields.append(fields.StringField(id='3', label='test', type='string'))
+    formdef.fields.append(fields.CommentField(id='4', label='test', type='comment'))
+    formdef.fields.append(fields.FileField(id='5', label='test', type='file', required=False))
+    formdef.store()
+
+    create_superuser(pub)
+    user_count = pub.user_class.count()
+    account_count = PasswordAccount.count()
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/')
+    resp = resp.click('New User')
+    resp.form['name'] = 'a new user'
+    resp.form['f3'] = 'TEST'
+    resp = resp.form.submit('submit')
+    assert resp.location == 'http://example.net/backoffice/users/'
+    resp = resp.follow()
+    assert 'a new user' in resp.text
+    resp = resp.click('a new user')
+    assert 'User - a new user' in resp.text
+    assert 'TEST' in resp.text
+    assert pub.user_class.count() == user_count + 1
+    assert PasswordAccount.count() == account_count
+
+
+def test_users_display_roles(pub):
+    pub.user_class.wipe()
+
+    user = create_superuser(pub)
+    role = create_role()
+    user.roles = [role.id, 'XXX']
+    user.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/users/%s/' % user.id)
+    assert role.name in resp.text
+    assert 'Unknown role (XXX)' in resp.text
-- 
2.29.2

