Repository: CSCfi/rems
Pull request #2669 (2021-05-17 13:01:55)

doc: update manual to include IP address form field
# Checklist for author

Remove items that aren't applicable, check items that are done.

## Reviewability
- [ ] Link to issue
- [ ] Note if PR is on top of other PR
- [ ] Note if related change in rems-deploy repo
- [ ] Consider adding screenshots for ease of review

## Backwards compatibility
- [ ] API is backwards compatible or completely new
- [ ] Events are backwards compatible _or_ have migrations
- [ ] Config is backwards compatible

## Documentation
- [ ] Update changelog if necessary
- [ ] API is documented and shows up in Swagger UI
- [ ] Components are added to guide page
- [ ] Update docs/ (if applicable)
- [ ] Update manual/ (if applicable)
- [ ] ADR for major architectural decisions or experiments
- [ ] New config options in config-defaults.edn

## Testing
- [ ] Complex logic is unit tested
- [ ] Valuable features are integration / browser / acceptance tested automatically

## Follow-up
- [ ] New tasks are created for pending or remaining tasks


marharyta: Would that be it or am I missing something?

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 95b68083b..ade585437 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -14,10 +14,10 @@ defaults: &defaults
 executors:
   clojure:
     docker:
-      - image: circleci/clojure:openjdk-16-lein-2.9.5-buster-node-browsers
+      - image: circleci/clojure:openjdk-11-lein-2.9.3-node-browsers
   db:
     docker:
-      - image: circleci/clojure:openjdk-16-lein-2.9.5-buster-node-browsers
+      - image: circleci/clojure:openjdk-11-lein-2.9.3-node-browsers
       - image: postgres:13
         environment:
           POSTGRES_HOST_AUTH_METHOD: trust
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7315be19e..150c3c41b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,20 +9,6 @@ have notable changes.
 Changes since v2.17
 
 ### Changes
-- The "Attachments (zip)" button in the UI now only downloads the current application attachments. Event attachments and previous versions of application attachments are left out. The full zip is still available via the API. (#2453)
-- Changes to theming: (#2588)
-  - Theme variables are now documented in [resources/config-defaults.edn](resources/config-defaults.edn).
-  - The `:nav-color` now simply defaults to `:link-color`. Previously, it defaulted to `:color3` if `:link-color` is unset.
-  - The theme variables `:danger-color` (didn't really affect anything) and `:phase-background-active` (wasn't used, overlaps with `:phase-bgcolor-active`) have been removed.
-  - The default theme has minor visual changes:
-    - color2 is lighter
-    - table hover highlight is now dark-on-light instead of light-on-dark
-    - link color used in nav bar
-  - Some theme-related code was rewritten. There should be no changes to appearance, but bugs are possible.
-- Setting `:log-authentication-details` now prints out details for failed OIDC HTTP requests.
-- More validations in the API:
-  - the values of multiselect fields (/api/applications/save-draft)
-  - option and column keys (/api/forms/create, /api/forms/edit)
 
 ### Fixes
 - Errors for invalid inputs (field values that are too long, invalid email addresses, etc.) are now rendered nicely. Previously the applicant just saw a "Save draft: Failed" message. (#2611)
@@ -33,8 +19,6 @@ Changes since v2.17
 ### Additions
 - REMS now supports PostgreSQL version 13. (#2642)
 - Experimental GA4GH Permissions API now allows users to query their own permissions via `/api/permissions/:user`. (#2631)
-- The handler can now change the applicant of a submitted application. (#2581)
-  - This feature can be disabled by adding `:application.command/change-applicant` to `:disable-commands`.
 
 ## v2.17 "Isokaari" 2021-04-12
 
diff --git a/Dockerfile b/Dockerfile
index fdfe7a85c..7bd197849 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-# For documentation see docs/installing-upgrading.md
+# For documentation see docs/docker.md
 
 FROM openjdk:11-jre-slim
 
diff --git a/docker-compose-build.yml b/docker-compose-build.yml
index 3b8f467f6..46cc0d931 100644
--- a/docker-compose-build.yml
+++ b/docker-compose-build.yml
@@ -1,5 +1,4 @@
 ### Example file for building REMS docker image and running it
-# See docs/installing-upgrading.md for more info
 
 version: '2'
 services:
@@ -15,12 +14,12 @@ services:
       AUTHENTICATION: :fake
       PUBLIC_URL: http://localhost:3000/
     mem_reservation: 200m
-    mem_limit: 2000m
+    mem_limit: 500m
     ports:
       - "127.0.0.1:3000:3000"
 
   db:
-    image: postgres:13
+    image: postgres:9.6
     environment:
       POSTGRES_USER: rems
       POSTGRES_PASSWORD: remspassword
diff --git a/docker-compose-config.yml b/docker-compose-config.yml
index 4ce96103c..374aad755 100644
--- a/docker-compose-config.yml
+++ b/docker-compose-config.yml
@@ -1,5 +1,4 @@
 ### Example file for running REMS using a config file
-# See docs/installing-upgrading.md for more info
 
 version: '2'
 services:
@@ -7,14 +6,14 @@ services:
   app:
     image: cscfi/rems
     mem_reservation: 200m
-    mem_limit: 2000m
+    mem_limit: 500m
     ports:
       - "127.0.0.1:3000:3000"
     volumes:
       - ./simple-config.edn:/rems/config/config.edn
 
   db:
-    image: postgres:9.6 # TODO upgrade to postgres:13 once REMS v2.18 is out
+    image: postgres:9.6
     environment:
       POSTGRES_USER: rems
       POSTGRES_PASSWORD: remspassword
diff --git a/docker-compose.yml b/docker-compose.yml
index 5a1208c20..3f19a991a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,4 @@
 ### Example file for running REMS using docker image from DockerHub
-# See docs/installing-upgrading.md for more info
 
 version: '2'
 services:
@@ -12,12 +11,12 @@ services:
       AUTHENTICATION: :fake
       PUBLIC_URL: http://localhost:3000/
     mem_reservation: 200m
-    mem_limit: 2000m
+    mem_limit: 500m
     ports:
       - "127.0.0.1:3000:3000"
 
   db:
-    image: postgres:9.6 # TODO upgrade to postgres:13 once REMS v2.18 is out
+    image: postgres:9.6
     environment:
       POSTGRES_USER: rems
       POSTGRES_PASSWORD: remspassword
diff --git a/docs/application-permissions.md b/docs/application-permissions.md
index ea9dfbcd5..ebb895d99 100644
--- a/docs/application-permissions.md
+++ b/docs/application-permissions.md
@@ -1,3 +1,3 @@
 # Application Permissions Reference
 
-<table border="1"><tr><th>State \ Role</th><th>applicant</th><th>decider</th><th>everyone‑else</th><th>handler</th><th>member</th><th>past‑decider</th><th>past‑reviewer</th><th>reporter</th><th>reviewer</th></tr><tr><th valign="top">approved</th><td valign="top"><!-- role: applicant --><div>accept‑licenses</div><div>copy‑as‑new</div><div>remove‑member</div><div>uninvite‑member</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div><div>decide</div><div>remark</div></td><td valign="top"><!-- role: everyone-else --><div>accept‑invitation</div></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>add‑member</div><div>change‑resources</div><div>close</div><div>invite‑member</div><div>remark</div><div>remove‑member</div><div>revoke</div><div>uninvite‑member</div></td><td valign="top"><!-- role: member --><div>accept‑licenses</div><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div><div>remark</div><div>review</div></td></tr><tr><th valign="top">closed</th><td valign="top"><!-- role: applicant --><div>copy‑as‑new</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div></td><td valign="top"><!-- role: everyone-else --></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: member --><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div></td></tr><tr><th valign="top">draft</th><td valign="top"><!-- role: applicant --><div>accept‑licenses</div><div>change‑resources</div><div>copy‑as‑new</div><div>delete</div><div>invite‑member</div><div>remove‑member</div><div>save‑draft</div><div>submit</div><div>uninvite‑member</div></td><td valign="top"><!-- role: decider --></td><td valign="top"><!-- role: everyone-else --><div>accept‑invitation</div></td><td valign="top"><!-- role: handler --></td><td valign="top"><!-- role: member --><div>accept‑licenses</div><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --></td><td valign="top"><!-- role: past-reviewer --></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --></td></tr><tr><th valign="top">rejected</th><td valign="top"><!-- role: applicant --><div>copy‑as‑new</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div></td><td valign="top"><!-- role: everyone-else --></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: member --><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div></td></tr><tr><th valign="top">returned</th><td valign="top"><!-- role: applicant --><div>accept‑licenses</div><div>change‑resources</div><div>close</div><div>copy‑as‑new</div><div>invite‑member</div><div>remove‑member</div><div>save‑draft</div><div>submit</div><div>uninvite‑member</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div><div>decide</div><div>remark</div></td><td valign="top"><!-- role: everyone-else --><div>accept‑invitation</div></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>add‑licenses</div><div>add‑member</div><div>assign‑external‑id</div><div>change‑resources</div><div>close</div><div>invite‑decider</div><div>invite‑member</div><div>invite‑reviewer</div><div>remark</div><div>remove‑member</div><div>request‑review</div><div>uninvite‑member</div></td><td valign="top"><!-- role: member --><div>accept‑licenses</div><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div><div>remark</div><div>review</div></td></tr><tr><th valign="top">revoked</th><td valign="top"><!-- role: applicant --><div>copy‑as‑new</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div></td><td valign="top"><!-- role: everyone-else --></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: member --><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div></td></tr><tr><th valign="top">submitted</th><td valign="top"><!-- role: applicant --><div>accept‑licenses</div><div>copy‑as‑new</div><div>remove‑member</div><div>uninvite‑member</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div><div>approve</div><div>decide</div><div>reject</div><div>remark</div></td><td valign="top"><!-- role: everyone-else --><div>accept‑invitation</div></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>add‑licenses</div><div>add‑member</div><div>approve</div><div>assign‑external‑id</div><div>change‑applicant</div><div>change‑resources</div><div>close</div><div>invite‑decider</div><div>invite‑member</div><div>invite‑reviewer</div><div>reject</div><div>remark</div><div>remove‑member</div><div>request‑decision</div><div>request‑review</div><div>return</div><div>uninvite‑member</div></td><td valign="top"><!-- role: member --><div>accept‑licenses</div><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div><div>remark</div><div>review</div></td></tr></table>
\ No newline at end of file
+<table border="1"><tr><th>State \ Role</th><th>applicant</th><th>decider</th><th>everyone‑else</th><th>handler</th><th>member</th><th>past‑decider</th><th>past‑reviewer</th><th>reporter</th><th>reviewer</th></tr><tr><th valign="top">approved</th><td valign="top"><!-- role: applicant --><div>accept‑licenses</div><div>copy‑as‑new</div><div>remove‑member</div><div>uninvite‑member</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div><div>decide</div><div>remark</div></td><td valign="top"><!-- role: everyone-else --><div>accept‑invitation</div></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>add‑member</div><div>change‑resources</div><div>close</div><div>invite‑member</div><div>remark</div><div>remove‑member</div><div>revoke</div><div>uninvite‑member</div></td><td valign="top"><!-- role: member --><div>accept‑licenses</div><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div><div>remark</div><div>review</div></td></tr><tr><th valign="top">closed</th><td valign="top"><!-- role: applicant --><div>copy‑as‑new</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div></td><td valign="top"><!-- role: everyone-else --></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: member --><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div></td></tr><tr><th valign="top">draft</th><td valign="top"><!-- role: applicant --><div>accept‑licenses</div><div>change‑resources</div><div>copy‑as‑new</div><div>delete</div><div>invite‑member</div><div>remove‑member</div><div>save‑draft</div><div>submit</div><div>uninvite‑member</div></td><td valign="top"><!-- role: decider --></td><td valign="top"><!-- role: everyone-else --><div>accept‑invitation</div></td><td valign="top"><!-- role: handler --></td><td valign="top"><!-- role: member --><div>accept‑licenses</div><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --></td><td valign="top"><!-- role: past-reviewer --></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --></td></tr><tr><th valign="top">rejected</th><td valign="top"><!-- role: applicant --><div>copy‑as‑new</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div></td><td valign="top"><!-- role: everyone-else --></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: member --><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div></td></tr><tr><th valign="top">returned</th><td valign="top"><!-- role: applicant --><div>accept‑licenses</div><div>change‑resources</div><div>close</div><div>copy‑as‑new</div><div>invite‑member</div><div>remove‑member</div><div>save‑draft</div><div>submit</div><div>uninvite‑member</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div><div>decide</div><div>remark</div></td><td valign="top"><!-- role: everyone-else --><div>accept‑invitation</div></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>add‑licenses</div><div>add‑member</div><div>assign‑external‑id</div><div>change‑resources</div><div>close</div><div>invite‑decider</div><div>invite‑member</div><div>invite‑reviewer</div><div>remark</div><div>remove‑member</div><div>request‑review</div><div>uninvite‑member</div></td><td valign="top"><!-- role: member --><div>accept‑licenses</div><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div><div>remark</div><div>review</div></td></tr><tr><th valign="top">revoked</th><td valign="top"><!-- role: applicant --><div>copy‑as‑new</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div></td><td valign="top"><!-- role: everyone-else --></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: member --><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div></td></tr><tr><th valign="top">submitted</th><td valign="top"><!-- role: applicant --><div>accept‑licenses</div><div>copy‑as‑new</div><div>remove‑member</div><div>uninvite‑member</div></td><td valign="top"><!-- role: decider --><div>see‑everything</div><div>approve</div><div>decide</div><div>reject</div><div>remark</div></td><td valign="top"><!-- role: everyone-else --><div>accept‑invitation</div></td><td valign="top"><!-- role: handler --><div>see‑everything</div><div>add‑licenses</div><div>add‑member</div><div>approve</div><div>assign‑external‑id</div><div>change‑resources</div><div>close</div><div>invite‑decider</div><div>invite‑member</div><div>invite‑reviewer</div><div>reject</div><div>remark</div><div>remove‑member</div><div>request‑decision</div><div>request‑review</div><div>return</div><div>uninvite‑member</div></td><td valign="top"><!-- role: member --><div>accept‑licenses</div><div>copy‑as‑new</div></td><td valign="top"><!-- role: past-decider --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: past-reviewer --><div>see‑everything</div><div>remark</div></td><td valign="top"><!-- role: reporter --><div>see‑everything</div></td><td valign="top"><!-- role: reviewer --><div>see‑everything</div><div>remark</div><div>review</div></td></tr></table>
\ No newline at end of file
diff --git a/docs/contrib/keycloak/keycloak.md b/docs/contrib/keycloak/keycloak.md
index 5cc47cdb4..1f266dd7f 100644
--- a/docs/contrib/keycloak/keycloak.md
+++ b/docs/contrib/keycloak/keycloak.md
@@ -137,9 +137,13 @@ services:
 ```
 Then, in your REMS config file, you must set something like:
 ```
-:oidc-metadata-url "http://keycloak:8080/auth/realms/rems-idp/.well-known/openid-configuration"
+:oidc-metadata-url "http://keycloak:8080/auth/realms/my-realm/.well-known/openid-configuration"`.
 ```
-If you use `localhost:8080` instead of `keycloak:8080`, then REMS will crash with a `Connection Refused` exception early in runtime. 
+If you instead set it to something like:
+```
+:oidc-metadata-url "http://localhost:8080/auth/realms/dycons-researcher-idp/.well-known/openid-configuration"
+```
+then REMS will crash with a `Connection Refused` exception early in runtime. 
 
 Using the `keycloak:8080` hostname, the problem arises when a user attempts to Login to REMS. REMS requests authentication redirection URLs from Keycloak at the `keycloak:8080` address. Keycloak then generates those URLs using the `keycloak:8080` hostname used in the request. However, `keycloak:8080` is meaningless to the user's browser, which does not have access to the docker-compose network namespace. The browser is unable to find Keycloak at that address.
 
diff --git a/docs/contrib/keycloak/simple-config.edn b/docs/contrib/keycloak/simple-config.edn
index 8ed98c019..ce9ff2ce1 100644
--- a/docs/contrib/keycloak/simple-config.edn
+++ b/docs/contrib/keycloak/simple-config.edn
@@ -6,7 +6,7 @@
  :authentication :oidc
 
  ; The location of Keycloak's OIDC metadata for the realm to which REMS is client
- :oidc-metadata-url "http://keycloak:8080/auth/realms/rems-idp/.well-known/openid-configuration"
+ :oidc-metadata-url "http://rp-keycloak:8080/auth/realms/dycons-researcher-idp/.well-known/openid-configuration"
 
  ; REMS' client ID, as defined in Keycloak
  :oidc-client-id "rems-client"
diff --git a/docs/docker.md b/docs/docker.md
new file mode 100644
index 000000000..e8f5327c8
--- /dev/null
+++ b/docs/docker.md
@@ -0,0 +1,8 @@
+# Deploy Rems in docker
+
+1. Build rems with lein or copy rems.jar to target/uberjar/rems.jar
+2. Build rems docker image. Use the docker file located in the root directory of rems git.
+Example command: `docker build /path/to/rems/git/root/dir/ -t rems:tag`
+2. Mount your config.edn file to /rems/config/config.edn in the container (Optional)
+3. Mount certificate to be added to rems certificate store to /rems/certs/.
+This process currecntly supports only one certificate. (Optional)
diff --git a/manual/handler.md b/manual/handler.md
index 98c716508..b0c075a8c 100644
--- a/manual/handler.md
+++ b/manual/handler.md
@@ -68,7 +68,6 @@ You can also edit the applications:
 
 - **Change the licenses**: The applicant will receive an email notification about the change in terms of use and has to accept the new licenses before they will be granted access rights.
 - **Add or remove members**: If you do not want to grant access to all the members in the application, you can remove additional members. Adding members is also possible.
-- **Change the applicant**: You can change the applicant to another member of the application. This can be useful if, for example, the original applicant has left the applying organization.
 - **Change the application’s resources**: If the applicant is applying for access rights to multiple resources, you can remove some of them if you do not want to grant access to all of them. It is also possible to add new resources if they can be applied for using the same application form. These will show as available in the drop-down list.
 
 You can comment on the application throughout the application process by selecting **Remark** and choosing if you want to show the comment to the applicant or not. Applicants will receive a notification about new comments. Sometimes it is useful for the applicant to see the comments so that they know how the approval process is proceeding. Note that the applicant cannot respond to the comments.
diff --git a/manual/owner.md b/manual/owner.md
index 4e11d548a..23a4b5930 100644
--- a/manual/owner.md
+++ b/manual/owner.md
@@ -60,6 +60,7 @@ Application forms are totally customisable. You can create different fields, suc
 **Attachment**: Applicant can add one or multiple attachments.
 **Label**: Use labels as descriptive texts if you want to give the applicant additional information, such as instructions, regarding some field.
 **Header**: Consider using headers if you want to divide the application form into sections.
+**IP address**: IP address field will validate the IP address the applicant has provided in IPv4 or IPv6 forms. IP address must not be within the known private range.
 
 Placeholder texts: It is possible to add placeholder text to most of the fields. Use placeholder texts to provide an example on how the field should be filled. Placeholders help the applicant to fill in the field correctly. For example:
 
diff --git a/resources/config-defaults.edn b/resources/config-defaults.edn
index 5deb81d42..428b1081f 100644
--- a/resources/config-defaults.edn
+++ b/resources/config-defaults.edn
@@ -32,7 +32,7 @@
  ;; Supported authentication methods: :oidc, :fake
  :authentication :fake
 
- ;; Log additional details about (OIDC) authentication. May leak secrets in the log, but useful for debugging.
+ ;; Log additional details about authentication
  :log-authentication-details false
 
  ;; Show link to an alternative login service
@@ -121,118 +121,9 @@
 
  ;; UI theme variables.
  :theme {:color1 "#eee"
-         :color2 "#999"
+         :color2 "#555"
          :color3 "#000"
-         :color4 "#000"
-
-         ;; Images & logos
-         :img-path  "../../img/" ; Path for static images, relative to /css/<lang>/screen.css
-         :logo-name nil ; Big logo, used on home page. Also used on other pages if :navbar-logo-name is not set.
-         :logo-name-en nil ; Localized big logo, defaults to :logo-name
-         :logo-name-sm nil ; Small version of big logo, used in mobile mode.
-         :logo-name-en-sm nil ; Localized small version of big logo, defaults to :logo-name-sm
-         :navbar-logo-name nil ; Small logo for navbar. Used on non-home pages instead of :logo-name.
-         :navbar-logo-name-en nil ; Localized small logo for navbar, defaults to :navbar-logo-name
-         :logo-bgcolor nil
-         :logo-content-origin nil
-
-         ;; Phases
-         :phase-bgcolor "#eee"
-         :phase-color "#111"
-         :phase-bgcolor-active "#ccc"
-         :phase-color-active nil ; defaults to :phase-color
-         :phase-bgcolor-completed "#ccc"
-         :phase-color-completed nil ; defaults to :phase-color
-
-         ;; Navbar
-         :button-navbar-font-weight 700
-         :navbar-color "#111" ; color of text in navbar
-         :nav-color nil ; color of links on navbar, defaults to :link-color
-         :nav-active-color nil ; defaults to :color4
-         :nav-hover-color nil ; defaults to :color4
-         :big-navbar-text-transform "none" ; can be used to e.g. uppercase the navbar text
-
-         ;; Tables
-         :table-hover-color nil ; defaults to :table-text-color
-         :table-hover-bgcolor nil ; defaults to :color2
-         :table-bgcolor nil ; defaults to :color1
-         :table-shadow nil
-         :table-border "1px solid #ccc"
-         :table-heading-color "#fff"
-         :table-heading-bgcolor nil ; defaults to :color3
-         :table-selection-bgcolor nil ; defaults to a darkened :table-hover-bgcolor, :table-bgcolor or :color3
-         :table-text-color nil ; defaults to bootstrap colors
-         :table-stripe-color nil ; background for every second table row, defaults to :color1
-
-         ;; Text
-         :header-border "3px solid #ccc"
-         :header-shadow nil ; defaults to :table-shadow
-         :link-color  "#025b96"
-         :link-hover-color nil ; defaults to color4
-         :font-family "'Lato', sans-serif"
-         ;; Text colors:
-         :text-primary nil
-         :bg-primary nil
-         :text-secondary nil
-         :bg-secondary nil
-         :text-success nil
-         :bg-success nil
-         :text-danger nil
-         :bg-danger nil
-         :text-warning "#ffc107!important"
-         :bg-warning nil
-         :text-info nil
-         :bg-info nil
-         :text-light nil
-         :bg-light nil
-         :text-dark nil
-         :bg-dark nil
-         :text-white nil
-         :bg-white nil
-         :text-muted nil
-
-         ;; Alerts
-         :alert-primary-color nil ; defaults to bootstrap colors
-         :alert-primary-bgcolor nil ; defaults to bootstrap colors
-         :alert-primary-bordercolor nil ; defaults to :alert-primary-color
-         :alert-secondary-color nil ; defaults to bootstrap colors
-         :alert-secondary-bgcolor nil ; defaults to bootstrap colors
-         :alert-secondary-bordercolor nil ; defaults to :alert-secondary-color
-         :alert-success-color nil ; defaults to bootstrap colors
-         :alert-success-bgcolor nil ; defaults to bootstrap colors
-         :alert-success-bordercolor nil ; defaults to :alert-success-color
-         :alert-danger-color nil ; defaults to bootstrap colors
-         :alert-danger-bgcolor nil ; defaults to bootstrap colors
-         :alert-danger-bordercolor nil ; defaults to :alert-danger-color
-         :alert-warning-color nil ; defaults to bootstrap colors
-         :alert-warning-bgcolor nil ; defaults to bootstrap colors
-         :alert-warning-bordercolor nil ; defaults to :alert-warning-color
-         :alert-info-color nil ; defaults to bootstrap colors
-         :alert-info-bgcolor nil ; defaults to bootstrap colors
-         :alert-info-bordercolor nil ; defaults to :alert-info-color
-         :alert-light-color nil ; defaults to bootstrap colors
-         :alert-light-bgcolor nil ; defaults to bootstrap colors
-         :alert-light-bordercolor nil ; defaults to :alert-light-color
-         :alert-dark-color nil ; defaults to bootstrap colors
-         :alert-dark-bgcolor nil ; defaults to bootstrap colors
-         :alert-dark-bordercolor nil ; defaults to :alert-dark-color
-
-         ;; Buttons
-         :primary-button-color "#fff"
-         :primary-button-bgcolor nil ; defaults to :color4
-         :primary-button-hover-color nil ; defaults to :primary-button-color
-         :primary-button-hover-bgcolor nil ; defaults to :primary-button-bgcolor
-         :secondary-button-color nil ; defaults to bootstrap colors
-         :secondary-button-bgcolor nil ; defaults to bootstrap colors
-         :secondary-button-hover-color nil ; defaults to bootstrap colors
-         :secondary-button-hover-bgcolor nil ; defaults to bootstrap colors
-
-         ;; Misc
-         :footer-color nil ; defaults to :table-heading-color
-         :footer-bgcolor nil ; defaults to :table-heading-bgcolor or color3
-         :collapse-shadow nil ; defaults to :table-shadow
-         :collapse-bgcolor "#fff"
-         :collapse-color "#fff"}
+         :color4 "#000"}
 
  ;; Optional extra static resources directory.
  :extra-static-resources nil
@@ -300,3 +191,4 @@
  :test-database-url nil
  :accessibility-report false ; should the axe accessibility tooling be included?
  :enable-ega false} ; should the EGA features be shown in the UI
+
diff --git a/resources/translations/en.edn b/resources/translations/en.edn
index 440983224..1a2f7d163 100644
--- a/resources/translations/en.edn
+++ b/resources/translations/en.edn
@@ -12,7 +12,6 @@
             :assign-external-id-info "External id"
             :bundling-intro [:div.intro [:p "Note that only resources that have the same application workflow can be bundled together."]]
             :cancel "Cancel"
-            :change-applicant "Make applicant"
             :change-resources "Change resources"
             :clear "Clear"
             :close "Close application"
@@ -140,8 +139,7 @@
                    :workflows "Workflows"}
   :applicant-info {:applicant "Applicant"
                    :applicants "Applicants"
-                   :email "Email"
-                   :email-idp "Email (from identity provider)"
+                   :email "Email (from identity provider)"
                    :invited-member "Invited member"
                    :member "Member"
                    :name "Name"
@@ -164,8 +162,7 @@
                  :entitlement-end "Access rights end %1."
                  ;; Format parameters for all events:
                  ;; %1 - name of event actor
-                 :events {:applicant-changed "%1 changed the applicant to %2."
-                          :approved "%1 approved the application."
+                 :events {:approved "%1 approved the application."
                           :closed "%1 closed the application."
                           :copied-from "This application is a copy of an old application."
                           :copied-to "%1 copied the application to a new application."
@@ -306,7 +303,7 @@
                 :type-visibility "Visible"
                 :visibility {:always "Always"
                              :field "Field"
-                             :has-value "Has one of the values"
+                             :has-value "Has value"
                              :hidden "Hidden"
                              :only-if "Only if"}}
   :create-license {:external-link "External link"
@@ -336,11 +333,7 @@
   ;; %4 - name of applicant
   ;; %5 - catalogue items of application, comma separated list
   ;; %6 - link to application
-  :email {:applicant-changed {:subject-to-member "Applicant for application %3 changed"
-                              :message-to-member "Dear %1,\n\nThe applicant for application %3 has been changed to %4 by %2.\n\nYou can view the application at %6."
-                              :subject-to-handler "(%3) Applicant changed"
-                              :message-to-handler "Dear %1,\n\nThe applicant for application %3 has been changed to %4 by %2.\n\nYou can view the application at %6."}
-          :application-approved {:subject-to-applicant "Your application %3 has been approved"
+  :email {:application-approved {:subject-to-applicant "Your application %3 has been approved"
                                  :message-to-applicant "Dear %1,\n\nYour application %3 has been approved.\n\nYou can view the application and the decision at %6"
                                  :subject-to-handler "(%3) Application has been approved"
                                  :message-to-handler "Dear %1,\n\n%2 has approved the application %3 from %4.\n\nYou can view the application and the decision at %6"}
@@ -415,7 +408,7 @@
          :application "Application"
          :attachment-remove "Remove"
          :attachments "Attachments"
-         :attachments-as-zip "Attachments (zip)"
+         :attachments-as-zip "All attachments (zip)"
          :checkbox-checked "Yes"
          :checkbox-unchecked "No"
          :comment "Comment"
diff --git a/resources/translations/fi.edn b/resources/translations/fi.edn
index 413775c81..b10cfeb87 100644
--- a/resources/translations/fi.edn
+++ b/resources/translations/fi.edn
@@ -12,7 +12,6 @@
             :assign-external-id-info "Tunniste"
             :bundling-intro [:div.intro [:p "Huomaa, että vain resursseja, joilla on sama työvuo, voidaan liittää yhteen."]]
             :cancel "Peruuta"
-            :change-applicant "Tee hakija"
             :change-resources "Muuta resursseja"
             :clear "Tyhjennä"
             :close "Sulje hakemus"
@@ -136,8 +135,7 @@
                    :workflows "Työvuot"}
   :applicant-info {:applicant "Hakija"
                    :applicants "Hakijat"
-                   :email "Sähköposti"
-                   :email-idp "Sähköposti (tunnistautumispalvelusta)"
+                   :email "Sähköposti (tunnistautumispalvelusta)"
                    :invited-member "Kutsuttu jäsen"
                    :member "Jäsen"
                    :name "Nimi"
@@ -155,8 +153,7 @@
                  :description "Otsikko"
                  :empty "Ei hakemuksia."
                  :entitlement-end "Pääsyoikeudet loppuvat %1."
-                 :events {:applicant-changed "%1 vaihtoi hakijan henkilöksi %2."
-                          :approved "%1 hyväksyi hakemuksen."
+                 :events {:approved "%1 hyväksyi hakemuksen. "
                           :closed "%1 sulki hakemuksen."
                           :copied-from "Tämä hakemus on kopio vanhasta hakemuksesta."
                           :copied-to "%1 kopioi hakemuksen uudeksi hakemukseksi."
@@ -281,7 +278,7 @@
                 :type-visibility "Näkyvissä"
                 :visibility {:always "Aina"
                              :field "Kentällä"
-                             :has-value "On yksi arvoista"
+                             :has-value "On arvo"
                              :hidden "Piilossa"
                              :only-if "Vain jos"}}
   :create-license {:external-link "Linkki"
@@ -304,11 +301,7 @@
                     :title "Työvuon nimi"}
   :dropdown {:no-results "Ei tuloksia"
              :placeholder "Valitse..."}
-  :email {:applicant-changed {:subject-to-member "Hakemuksen %3 hakija vaihtunut"
-                              :message-to-member "Hyvä %1,\n\n%2 on vaihtanut hakemuksen %3 hakijan käyttäjäksi %4.\n\nVoit tarkastella hakemusta: %6."
-                              :subject-to-handler "(%3) Hakija vaihtunut"
-                              :message-to-handler "Hyvä %1,\n\n%2 on vaihtanut hakemuksen %3 hakijan käyttäjäksi %4.\n\nVoit tarkastella hakemusta: %6."}
-          :application-approved {:subject-to-applicant "Hakemuksesi %3 on hyväksytty"
+  :email {:application-approved {:subject-to-applicant "Hakemuksesi %3 on hyväksytty"
                                  :message-to-applicant "Hyvä %1,\n\nHakemuksesi %3 on hyväksytty.\n\nVoit tarkastella hakemusta ja päätöstä osoitteessa %6"
                                  :subject-to-handler "(%3) Hakemus on hyväksytty"
                                  :message-to-handler "Hyvä %1,\n\n%2 on hyväksynyt käyttäjän %4 hakemuksen %3.\n\nVoit tarkastella hakemusta ja päätöstä osoitteessa %6"}
@@ -374,7 +367,7 @@
          :application "Tiedot"
          :attachment-remove "Poista"
          :attachments "Liitteet"
-         :attachments-as-zip "Liitteet (zip)"
+         :attachments-as-zip "Kaikki liiteet (zip)"
          :checkbox-checked "Kyllä"
          :checkbox-unchecked "Ei"
          :comment "Kommentti"
diff --git a/resources/translations/sv.edn b/resources/translations/sv.edn
index 1ebbf7f49..8ed14a964 100644
--- a/resources/translations/sv.edn
+++ b/resources/translations/sv.edn
@@ -12,7 +12,6 @@
             :assign-external-id-info "Externt id"
             :bundling-intro [:div.intro [:p "Notera att endast resurser med gemensam ansökningsblankett och gemensamt arbetsflöde kan kopplas samman."]]
             :cancel "Avbryt"
-            :change-applicant "Gör till sökanden" ; TODO check
             :change-resources "Ändra resurser"
             :clear "Töm"
             :close "Stäng ansökan"
@@ -136,8 +135,7 @@
                    :workflows "Arbetsflöden"}
   :applicant-info {:applicant "Sökande"
                    :applicants "Sökande"
-                   :email "E-postadress"
-                   :email-idp "E-postadress (från identifieringstjäst)"
+                   :email "E-postadress (från identifieringstjäst)"
                    :invited-member "Inbjuden deltagare"
                    :member "Deltagare"
                    :name "Namn"
@@ -155,8 +153,7 @@
                  :description "Rubrik"
                  :empty "Inga ansökningar."
                  :entitlement-end "Upphörandedatum för rättigheterna: %1."
-                 :events {:applicant-changed "%1 bytte sökande till %2." ; TODO check
-                          :approved "%1 godkände ansökan."
+                 :events {:approved "%1 godkände ansökan."
                           :closed "%1 stängde ansökan."
                           :copied-from "Den här ansökan är en kopia från en tidigare ansökan."
                           :copied-to "%1 kopierade ansökan som ny ansökan."
@@ -281,7 +278,7 @@
                 :type-visibility "Synlighet"
                 :visibility {:always "Visas alltid"
                              :field "Fält"
-                             :has-value "Har ett av värden"
+                             :has-value "Har värde"
                              :hidden "Dold"
                              :only-if "Visas om"}}
   :create-license {:external-link "Länk"
@@ -304,11 +301,7 @@
                     :title "Arbetsflödets namn"}
   :dropdown {:no-results "Inga resultat"
              :placeholder "Välj..."}
-  :email {:applicant-changed {:subject-to-member "Sökande för ansökan %3 bytte"
-                              :message-to-member "%1,\n\nSökande för ansökan %3 har bytts ut till %4 av %2.\n\nDu kan granska ansökningen här: %6." ; TODO check
-                              :subject-to-handler "(%3) Sökande bytte"
-                              :message-to-handler "%1,\n\nSökande för ansökan %3 har bytts ut till %4 av %2.\n\nDu kan granska ansökningen här: %6."} ; TODO check
-          :application-approved {:subject-to-applicant "Din ansökan %3 är godkänd"
+  :email {:application-approved {:subject-to-applicant "Din ansökan %3 är godkänd"
                                  :message-to-applicant "%1,\n\nDin ansökan %3 är godkänd.\n\nDu kan granska ansökningen och beslutet här: %6"
                                  :subject-to-handler "(%3) Ansökan är godkänd"
                                  :message-to-handler "%1,\n\n%2 har godkänt ansökan %3 från %4.\n\nDu kan granska ansökningen och beslutet här: %6"}
@@ -374,7 +367,7 @@
          :application "Ansökan"
          :attachment-remove "Ta bort"
          :attachments "Bilagor"
-         :attachments-as-zip "Bilagor (zip)"
+         :attachments-as-zip "Alla bilagor (zip)"
          :checkbox-checked "Ja"
          :checkbox-unchecked "Nej"
          :comment "Kommentar"
diff --git a/src/clj/rems/api/applications.clj b/src/clj/rems/api/applications.clj
index fe0b68728..54d2dff47 100644
--- a/src/clj/rems/api/applications.clj
+++ b/src/clj/rems/api/applications.clj
@@ -238,8 +238,6 @@
     (command-endpoint :application.command/invite-decider commands/InviteDeciderCommand)
     (command-endpoint :application.command/invite-member commands/InviteMemberCommand)
     (command-endpoint :application.command/invite-reviewer commands/InviteReviewerCommand)
-    (command-endpoint :application.command/change-applicant commands/ChangeApplicantCommand
-                      "Promote member of application to applicant. Previous applicant becomes a member.")
     (command-endpoint :application.command/reject commands/RejectCommand)
     (command-endpoint :application.command/remark commands/RemarkCommand)
     (command-endpoint :application.command/remove-member commands/RemoveMemberCommand)
@@ -274,14 +272,13 @@
         (api-util/not-found-json-response)))
 
     (GET "/:application-id/attachments" []
-      :summary "Get attachments for an application as a zip file"
+      :summary "Get all attachments as a zip file"
       :roles #{:logged-in}
       :path-params [application-id :- (describe s/Int "application id")]
-      :query-params [{all :- (describe s/Bool "Defaults to true. If set to false, the zip will only contain latest application attachments: no previous versions of attachments, and no event attachments.") true}]
       :responses {200 {}
                   404 {:schema s/Str :description "Not found"}}
       (if-let [app (applications/get-application-for-user (getx-user-id) application-id)]
-        (attachment/zip-attachments app all)
+        (attachment/zip-attachments app)
         (api-util/not-found-json-response)))
 
     (GET "/:application-id/experimental/pdf" request
diff --git a/src/clj/rems/api/services/attachment.clj b/src/clj/rems/api/services/attachment.clj
index 2177e3f1a..98760909e 100644
--- a/src/clj/rems/api/services/attachment.clj
+++ b/src/clj/rems/api/services/attachment.clj
@@ -2,7 +2,6 @@
   (:require [clojure.set :as set]
             [clojure.tools.logging :as log]
             [rems.application.commands :as commands]
-            [rems.application.model :as model]
             [rems.auth.util :refer [throw-forbidden]]
             [rems.db.applications :as applications]
             [rems.db.attachments :as attachments]
@@ -44,23 +43,21 @@
       (throw-forbidden))
     (attachments/save-attachment! file user-id application-id)))
 
-(defn zip-attachments [application all?]
-  (let [classes (model/classify-attachments application)
-        out (ByteArrayOutputStream.)]
+(defn zip-attachments [application]
+  (let [out (ByteArrayOutputStream.)]
     (with-open [zip (ZipOutputStream. out)]
       (doseq [metadata (getx application :application/attachments)]
         (let [id (getx metadata :attachment/id)
               attachment (attachments/get-attachment id)]
-          (when (or all? (contains? (get classes id) :field/value))
-            ;; we deduplicate filenames when uploading, but here's a
-            ;; failsafe in case we have duplicate filenames in old
-            ;; applications
-            (try
-              (.putNextEntry zip (ZipEntry. (getx attachment :attachment/filename)))
-              (.write zip (getx attachment :attachment/data))
-              (.closeEntry zip)
-              (catch ZipException e
-                (log/warn "Ignoring attachment" (pr-str metadata) "when generating zip. Cause:" e)))))))
+          ;; we deduplicate filenames when uploading, but here's a
+          ;; failsafe in case we have duplicate filenames in old
+          ;; applications
+          (try
+            (.putNextEntry zip (ZipEntry. (getx attachment :attachment/filename)))
+            (.write zip (getx attachment :attachment/data))
+            (.closeEntry zip)
+            (catch ZipException e
+              (log/warn "Ignoring attachment" (pr-str metadata) "when generating zip. Cause:" e))))))
     (-> (ok (ByteArrayInputStream. (.toByteArray out))) ;; extra copy of the data here, could be more efficient
         (header "Content-Disposition" (str "attachment;filename=attachments-" (getx application :application/id) ".zip"))
         (content-type "application/zip"))))
diff --git a/src/clj/rems/api/services/todos.clj b/src/clj/rems/api/services/todos.clj
index 2f457606b..f0dab9c60 100644
--- a/src/clj/rems/api/services/todos.clj
+++ b/src/clj/rems/api/services/todos.clj
@@ -46,7 +46,6 @@
           :application.command/add-licenses
           :application.command/add-member
           :application.command/assign-external-id
-          :application.command/change-applicant
           :application.command/change-resources
           :application.command/invite-member
           :application.command/invite-reviewer
diff --git a/src/clj/rems/api/util.clj b/src/clj/rems/api/util.clj
index 0e86480a5..cd5e54d2a 100644
--- a/src/clj/rems/api/util.clj
+++ b/src/clj/rems/api/util.clj
@@ -17,14 +17,13 @@
 (defn add-roles-documentation [summary roles]
   (when (nil? summary)
     (throw (IllegalArgumentException. "Route must have a :summary when using :roles and it must be specified before :roles")))
-  (let [role-str (->> roles
-                      (map name)
-                      (sort)
-                      (str/join ", "))]
-    `(str ~summary
-          " (roles: "
-          ~role-str
-          ")")))
+  (str summary
+       " (roles: "
+       (->> roles
+            (map name)
+            (sort)
+            (str/join ", "))
+       ")"))
 
 (defmethod restructure-param :roles [_ roles acc]
   (-> acc
diff --git a/src/clj/rems/application/commands.clj b/src/clj/rems/application/commands.clj
index a38ecd98e..a314835ac 100644
--- a/src/clj/rems/application/commands.clj
+++ b/src/clj/rems/application/commands.clj
@@ -74,9 +74,6 @@
   (assoc CommandBase
          :member {:name s/Str
                   :email s/Str}))
-(s/defschema ChangeApplicantCommand
-  (assoc CommandWithComment
-         :member schema-base/User))
 (s/defschema RejectCommand
   CommandWithComment)
 (s/defschema RemarkCommand
@@ -118,7 +115,6 @@
    :application.command/add-member AddMemberCommand
    :application.command/approve ApproveCommand
    :application.command/assign-external-id AssignExternalIdCommand
-   :application.command/change-applicant ChangeApplicantCommand
    :application.command/change-resources ChangeResourcesCommand
    :application.command/close CloseCommand
    :application.command/copy-as-new CopyAsNewCommand
@@ -564,15 +560,6 @@
                                    {:event/type :application.event/member-uninvited
                                     :application/member (:member cmd)})))
 
-(defmethod command-handler :application.command/change-applicant
-  [cmd application injections]
-  (or (when-not (contains? (set (map :userid (:application/members application)))
-                           (:userid (:member cmd)))
-        {:errors [{:type :user-not-member :user (:member cmd)}]})
-      (add-comment-and-attachments cmd injections
-                                   {:event/type :application.event/applicant-changed
-                                    :application/applicant (:member cmd)})))
-
 (defn- copy-field-values! [copy-attachment! application new-application-id]
   (vec
    (for [form (:application/forms application)
diff --git a/src/clj/rems/application/events.clj b/src/clj/rems/application/events.clj
index bfb394327..c63da6875 100644
--- a/src/clj/rems/application/events.clj
+++ b/src/clj/rems/application/events.clj
@@ -111,10 +111,6 @@
          :event/type (s/enum :application.event/member-uninvited)
          :application/member {:name s/Str
                               :email s/Str}))
-(s/defschema ApplicantChangedEvent
-  (assoc EventWithComment
-         :event/type (s/enum :application.event/applicant-changed)
-         :application/applicant schema-base/User))
 (s/defschema ReviewerInvitedEvent
   (assoc EventWithComment
          :event/type (s/enum :application.event/reviewer-invited)
@@ -164,8 +160,7 @@
          :event/type (s/enum :application.event/submitted)))
 
 (def event-schemas
-  {:application.event/applicant-changed ApplicantChangedEvent
-   :application.event/approved ApprovedEvent
+  {:application.event/approved ApprovedEvent
    :application.event/closed ClosedEvent
    :application.event/review-requested ReviewRequestedEvent
    :application.event/reviewed ReviewedEvent
diff --git a/src/clj/rems/application/master_workflow.clj b/src/clj/rems/application/master_workflow.clj
index 38b93cb43..6c7d26340 100644
--- a/src/clj/rems/application/master_workflow.clj
+++ b/src/clj/rems/application/master_workflow.clj
@@ -46,7 +46,6 @@
     :application.command/invite-member
     :application.command/invite-reviewer
     :application.command/uninvite-member
-    :application.command/change-applicant
     :application.command/request-review
     :application.command/request-decision
     :application.command/return
@@ -56,7 +55,6 @@
 
 (def ^:private handler-returned-commands
   (disj handler-all-commands
-        :application.command/change-applicant ;; could allow this, but decided to not do it right now
         :application.command/return
         :application.command/approve
         :application.command/reject
@@ -142,18 +140,6 @@
   (-> application
       (permissions/remove-role-from-user :member (get-in event [:application/member :userid]))))
 
-(defmethod application-permissions-view :application.event/applicant-changed
-  [application event]
-  (let [old (first (for [[user roles] (:application/user-roles application)
-                         :when (contains? roles :applicant)]
-                     user))
-        new (get-in event [:application/applicant :userid])]
-    (-> application
-        (permissions/remove-role-from-user :applicant old)
-        (permissions/give-role-to-users :member [old])
-        (permissions/give-role-to-users :applicant [new])
-        (permissions/remove-role-from-user :member new))))
-
 (defmethod application-permissions-view :application.event/submitted
   [application _event]
   (-> application
diff --git a/src/clj/rems/application/model.clj b/src/clj/rems/application/model.clj
index c60ff43f0..602523b74 100644
--- a/src/clj/rems/application/model.clj
+++ b/src/clj/rems/application/model.clj
@@ -107,13 +107,6 @@
       (update :application/members disj (:application/member event))
       (update :application/past-members conj (:application/member event))))
 
-(defmethod application-base-view :application.event/applicant-changed
-  [application event]
-  (-> application
-      (update :application/members disj (:application/applicant event))
-      (update :application/members conj (:application/applicant application))
-      (assoc :application/applicant (:application/applicant event))))
-
 (defmethod application-base-view :application.event/decider-invited
   [application event]
   (-> application
@@ -276,7 +269,6 @@
     {:permission :application.command/add-licenses}
     {:permission :application.command/add-member}
     {:permission :application.command/assign-external-id}
-    {:permission :application.command/change-applicant}
     {:permission :application.command/change-resources}
     {:permission :application.command/close}
     {:permission :application.command/copy-as-new}
@@ -307,7 +299,6 @@
     {:permission :application.command/add-licenses}
     {:permission :application.command/add-member}
     {:permission :application.command/assign-external-id}
-    {:permission :application.command/change-applicant}
     {:permission :application.command/change-resources}
     {:permission :application.command/close}
     {:permission :application.command/copy-as-new}
@@ -495,43 +486,8 @@
               :application.event/member-removed)
              {:application/member (get-user (:userid (:application/member event)))}
 
-             :application.event/applicant-changed
-             {:application/applicant (get-user (:userid (:application/applicant event)))}
-
              {}))))
 
-(defn classify-attachments [application]
-  (let [from-events (for [event (:application/events application)
-                          attachment (:event/attachments event)]
-                      {(:attachment/id attachment) #{:event/attachments}})
-        from-fields (for [form (getx application :application/forms)
-                          field (getx form :form/fields)
-                          :when (= :attachment (:field/type field))
-                          k [:field/value :field/previous-value]
-                          id (form/parse-attachment-ids (get field k))]
-                      {id #{k}})]
-    (apply merge-with set/union (concat from-events from-fields))))
-
-(deftest test-classify-attachments
-  (let [application {:application/events [{:event/type :application.event/foo
-                                           :event/attachments [{:attachment/id 1}
-                                                               {:attachment/id 3}]}
-                                          {:event/type :application.event/bar}]
-                     :application/forms [{:form/fields [{:field/type :attachment
-                                                         :field/value "5" :field/previous-value "5,1,15"}]}
-                                         {:form/fields [{:field/type :text
-                                                         :field/value "2" :field/previous-vaule "2"}
-                                                        {:field/type :attachment
-                                                         :field/value "9,11,13"}]}]}]
-    (is (= {1 #{:event/attachments :field/previous-value}
-            3 #{:event/attachments}
-            5 #{:field/value :field/previous-value}
-            9 #{:field/value}
-            11 #{:field/value}
-            13 #{:field/value}
-            15 #{:field/previous-value}}
-           (classify-attachments application)))))
-
 (defn- get-blacklist [application blacklisted?]
   (let [all-members (application-util/applicant-and-members application)
         all-resources (distinct (map :resource/ext-id (:application/resources application)))]
@@ -649,8 +605,7 @@
                                   :application.event/decider-joined
                                   :application.event/decision-requested})
 (deftest test-sensitive-events
-  (let [public-events #{:application.event/applicant-changed
-                        :application.event/approved
+  (let [public-events #{:application.event/approved
                         :application.event/closed
                         :application.event/copied-from
                         :application.event/copied-to
@@ -760,11 +715,18 @@
     (contains? (::latest-decision-request-by-user application) user-id)
     (assoc :application/todo :waiting-for-your-decision)))
 
+
 (defn- visible-attachment-ids [application]
-  (->> application
-       classify-attachments
-       keys
-       set))
+  (let [from-events (->> (:application/events application)
+                         (mapcat :event/attachments)
+                         (map :attachment/id))
+        from-fields (for [form (getx application :application/forms)
+                          field (getx form :form/fields)
+                          :when (= :attachment (:field/type field))
+                          value [(:field/value field) (:field/previous-value field)]
+                          attachment (form/parse-attachment-ids value)]
+                      attachment)]
+    (set (concat from-events from-fields))))
 
 (deftest test-visible-attachment-ids
   (let [application {:application/events [{:event/type :application.event/foo
diff --git a/src/clj/rems/auth/oidc.clj b/src/clj/rems/auth/oidc.clj
index 252d34821..07f9c227b 100644
--- a/src/clj/rems/auth/oidc.clj
+++ b/src/clj/rems/auth/oidc.clj
@@ -47,15 +47,11 @@
   (let [response (-> (http/post (:token_endpoint oidc-configuration)
                                 ;; NOTE Some IdPs don't support client id and secret in form params,
                                 ;;      and require us to use HTTP basic auth
-                                {:basic-auth [(str (getx env :oidc-client-id))
+                                {:basic-auth [(getx env :oidc-client-id)
                                               (getx env :oidc-client-secret)]
                                  :form-params {:grant_type "authorization_code"
                                                :code (get-in request [:params :code])
-                                               :redirect_uri (str (getx env :public-url) "oidc-callback")}
-                                 ;; Setting these will cause the exceptions raised by http/post to contain
-                                 ;; the request body, useful for debugging failures.
-                                 :save-request? (getx env :log-authentication-details)
-                                 :debug-body (getx env :log-authentication-details)})
+                                               :redirect_uri (str (getx env :public-url) "oidc-callback")}})
                      ;; FIXME Complains about Invalid cookie header in logs
                      ;; TODO Unhandled responses for token endpoint:
                      ;;      403 {\"error\":\"invalid_grant\",\"error_description\":\"Invalid authorization code\"} when reusing codes
diff --git a/src/clj/rems/css/style_utils.clj b/src/clj/rems/css/style_utils.clj
index 81630a031..5cf0242e1 100644
--- a/src/clj/rems/css/style_utils.clj
+++ b/src/clj/rems/css/style_utils.clj
@@ -2,32 +2,13 @@
   "The namespace contains the helpful style utils that are meant to be kept separate
    the main styles file to keep it cleaner."
   (:require [clojure.string :as str]
+            [garden.stylesheet :as stylesheet]
             [rems.config :refer [env]]))
 
 
-;; Customizable theme related functions
-
-(def ^:private ignore-theme-var-error?
-  ;; We don't want to include all these localized versions in
-  ;; config-defaults.edn. Luckily there are only a few so we can just
-  ;; have a whitelist.
-  #{:logo-name-fi
-    :logo-name-fi-sm
-    :navbar-logo-name-fi
-    :logo-name-sv
-    :logo-name-sv-sm
-    :navbar-logo-name-sv})
-
-(defn- theme-getx-impl
-  "Helper for making sure we document all our theme variables."
-  [attr]
-  (if-let [[_ v] (find (:theme env) attr)] ; find instead of get: nil values are ok, missing values are bad
-    v
-    (when (:dev env)
-      (when-not (ignore-theme-var-error? attr)
-        (assert false (str "Theme attribute " attr " used but not documented in config-defaults.edn!"))))))
-
-(defn theme-getx
+;; Customazable theme related functions
+
+(defn get-theme-attribute
   "Fetch the attribute value from the current theme with fallbacks.
 
   Keywords denote attribute lookups while strings are interpreted as fallback constant value."
@@ -35,7 +16,7 @@
   (when (seq attr-names)
     (let [attr-name (first attr-names)
           attr-value (if (keyword? attr-name)
-                       (theme-getx-impl attr-name)
+                       (get (:theme env) attr-name)
                        attr-name)]
       (or attr-value (recur (rest attr-names))))))
 
@@ -43,14 +24,15 @@
   (when path
     (let [url (if (str/starts-with? path "http")
                 path
-                (str (theme-getx :img-path) path))]
+                (str (get-theme-attribute :img-path "../../img/") path))]
       (str "url(\"" url "\")"))))
 
 (defn get-logo-image [lang]
-  (resolve-image (theme-getx (keyword (str "logo-name-" (name lang))) :logo-name)))
+  (resolve-image (get-theme-attribute (keyword (str "logo-name-" (name lang))) :logo-name)))
 
 (defn get-logo-name-sm [lang]
-  (resolve-image (theme-getx (keyword (str "logo-name-" (name lang) "-sm")) :logo-name-sm)))
+  (resolve-image (get-theme-attribute (keyword (str "logo-name-" (name lang) "-sm")) :logo-name-sm)))
 
 (defn get-navbar-logo [lang]
-  (resolve-image (theme-getx (keyword (str "navbar-logo-name-" (name lang))) :navbar-logo-name)))
+  (resolve-image (get-theme-attribute (keyword (str "navbar-logo-name-" (name lang))) :navbar-logo-name)))
+
diff --git a/src/clj/rems/css/styles.clj b/src/clj/rems/css/styles.clj
index f16ddd547..8d98d3840 100644
--- a/src/clj/rems/css/styles.clj
+++ b/src/clj/rems/css/styles.clj
@@ -20,7 +20,7 @@
             [mount.core :as mount]
             [rems.config :refer [env]]
             [rems.context :as context]
-            [rems.css.style-utils :refer [theme-getx get-logo-image get-navbar-logo get-logo-name-sm]]
+            [rems.css.style-utils :refer [get-theme-attribute get-logo-image get-navbar-logo get-logo-name-sm]]
             [ring.util.response :as response]))
 
 (def content-width (u/px 1200))
@@ -84,12 +84,12 @@
 (defn- generate-logo-styles []
   (list
    [:.logo-menu {:height logo-height-menu
-                 :background-color (theme-getx :logo-bgcolor)
+                 :background-color (get-theme-attribute :logo-bgcolor)
                  :width "100px"
                  :padding-top "0px"
                  :padding-bottom "0px"}]
    [:.logo {:height logo-height
-            :background-color (theme-getx :logo-bgcolor)
+            :background-color (get-theme-attribute :logo-bgcolor)
             :width "100%"
             :margin "0 auto"
             :padding "0 20px"
@@ -97,20 +97,20 @@
    [(s/descendant :.logo :.img)
     (s/descendant :.logo-menu :.img)
     {:height "100%"
-     :background-color (theme-getx :logo-bgcolor)
+     :background-color (get-theme-attribute :logo-bgcolor)
      :-webkit-background-size :contain
      :-moz-o-background-size :contain
      :-o-background-size :contain
      :background-size :contain
      :background-repeat :no-repeat
      :background-position [[:center :center]]
-     :background-origin (theme-getx :logo-content-origin)}]
+     :background-origin (get-theme-attribute :logo-content-origin)}]
    [(s/descendant :.logo :.img) {:background-image (get-logo-image context/*lang*)}]
    [(s/descendant :.logo-menu :.img) {:background-image (get-navbar-logo context/*lang*)}]
    (stylesheet/at-media {:max-width (u/px 480)}
                         (list
                          [(s/descendant :.logo :.img)
-                          {:background-color (theme-getx :logo-bgcolor)
+                          {:background-color (get-theme-attribute :logo-bgcolor)
                            :background-image (get-logo-name-sm context/*lang*)
                            :-webkit-background-size :contain
                            :-moz-background-size :contain
@@ -128,8 +128,8 @@
              :flex-direction "row"
              :justify-content "stretch"
              :align-items "center"}
-   [:.phase {:background-color (theme-getx :phase-bgcolor)
-             :color (theme-getx :phase-color)
+   [:.phase {:background-color (get-theme-attribute :phase-bgcolor "#eee")
+             :color (get-theme-attribute :phase-color "#111")
              :flex-grow 1
              :height (u/px 40)
              :display "flex"
@@ -153,12 +153,14 @@
                                           :border-left [[(u/px 10) :solid :white]]
                                           :border-bottom [[(u/px 20) :solid :transparent]]
                                           :border-right "none"}]
-    [:&.active {:color (theme-getx :phase-color-active)
-                :background-color (theme-getx :phase-bgcolor-active)
-                :border-color (theme-getx :phase-bgcolor-active)}]
-    [:&.completed {:color (theme-getx :phase-color-completed)
-                   :background-color (theme-getx :phase-bgcolor-completed)
-                   :border-color (theme-getx :phase-bgcolor-completed)}]]])
+    [:&.active (merge {:color (get-theme-attribute :phase-color-active :phase-color "#111")}
+                      (if-let [background (get-theme-attribute :phase-background-active)]
+                        {:background background}
+                        {:background-color (get-theme-attribute :phase-bgcolor-active "#ccc")
+                         :border-color (get-theme-attribute :phase-bgcolor-active "#ccc")}))]
+    [:&.completed {:background-color (get-theme-attribute :phase-bgcolor-completed "#ccc")
+                   :border-color (get-theme-attribute :phase-bgcolor-completed "#ccc")
+                   :color (get-theme-attribute :phase-color-completed :phase-color)}]]])
 
 (defn- generate-actions-float-menu
   "The #actions floating menu can be too long for some screens. There is no clean solution for this in pure CSS
@@ -181,17 +183,16 @@
                                     :overflow-y :auto
                                     :max-height "calc(100vh - 105px)"}])))
 
-;; TODO inline me
 (defn- button-navbar-font-weight []
   ;; Default font-weight to 700 so the text is considered
   ;; 'large text' and thus requires smaller contrast ratio for
   ;; accessibility.
-  (theme-getx :button-navbar-font-weight))
+  (get-theme-attribute :button-navbar-font-weight 700))
 
 (defn table-selection-bgcolor []
-  (if-let [selection-bgcolor (theme-getx :table-selection-bgcolor)]
+  (if-let [selection-bgcolor (get-theme-attribute :table-selection-bgcolor)]
     selection-bgcolor
-    (-> (theme-getx :table-hover-bgcolor :table-bgcolor :color3)
+    (-> (get-theme-attribute :table-hover-bgcolor :table-bgcolor :color3 "#777777")
         (c/darken 15))))
 
 (defn- generate-rems-table-styles []
@@ -199,7 +200,7 @@
    [:.rems-table.cart {:background "#fff"
                        :color "#000"
                        :margin 0}
-    [".cart-bundle:not(:last-child)" {:border-bottom [[(u/px 1) :solid (theme-getx :color1)]]}]
+    [".cart-bundle:not(:last-child)" {:border-bottom [[(u/px 1) :solid (get-theme-attribute :color1)]]}]
     [:td:before {:content "initial"}]
     [:th
      :td:before
@@ -208,15 +209,15 @@
      [(s/& (s/nth-child "2n")) {:background "#fff"}]]]
    [:.table-border {:padding 0
                     :margin "1em 0"
-                    :border (theme-getx :table-border)
+                    :border (get-theme-attribute :table-border "1px solid #ccc")
                     :border-radius (u/rem 0.4)}]
    [:.rems-table {:min-width "100%"
-                  :background-color (theme-getx :table-bgcolor :color1)
-                  :box-shadow (theme-getx :table-shadow)
-                  :color (theme-getx :table-text-color)}
+                  :background-color (get-theme-attribute :table-bgcolor :color1)
+                  :box-shadow (get-theme-attribute :table-shadow)
+                  :color (get-theme-attribute :table-text-color)}
     [:th {:white-space "nowrap"
-          :color (theme-getx :table-heading-color)
-          :background-color (theme-getx :table-heading-bgcolor :color3)}]
+          :color (get-theme-attribute :table-heading-color "#fff")
+          :background-color (get-theme-attribute :table-heading-bgcolor :color3)}]
     [:th
      :td
      {:text-align "left"
@@ -224,21 +225,21 @@
     [:.selection {:width (u/rem 0.5)
                   :padding-right 0}]
     [:td:before
-     {:color (theme-getx :table-text-color)}]
+     {:color (get-theme-attribute :table-text-color)}]
     [:tr {:margin "0 1rem"}
-     [:&:hover {:color (theme-getx :table-hover-color :table-text-color)
-                :background-color (theme-getx :table-hover-bgcolor :color2)}]
-     [:&.selected {:background-color (theme-getx :table-selection-bgcolor (table-selection-bgcolor))}]
+     [:&:hover {:color (get-theme-attribute :table-hover-color :table-text-color "#fff")
+                :background-color (get-theme-attribute :table-hover-bgcolor :color2)}]
+     [:&.selected {:background-color (get-theme-attribute :table-selection-bgcolor (table-selection-bgcolor))}]
      [(s/& (s/nth-child "2n"))
-      [:&:hover {:color (theme-getx :table-hover-color :table-text-color)
-                 :background-color (theme-getx :table-hover-bgcolor :color2)}]
-      {:background-color (theme-getx :table-stripe-color :table-bgcolor :color1)}
-      [:&.selected {:background-color (theme-getx :table-selection-bgcolor (table-selection-bgcolor))}]]]
+      [:&:hover {:color (get-theme-attribute :table-hover-color :table-text-color "#fff")
+                 :background-color (get-theme-attribute :table-hover-bgcolor :color2)}]
+      {:background-color (get-theme-attribute :table-stripe-color :table-bgcolor :color1)}
+      [:&.selected {:background-color (get-theme-attribute :table-selection-bgcolor (table-selection-bgcolor))}]]]
     [:td.commands:last-child {:text-align "right"
                               :padding-right (u/rem 1)}]]
    [:.rems-table.cart {:box-shadow :none}]
    [:.inner-cart {:margin (u/em 1)}]
-   [:.outer-cart {:border [[(u/px 1) :solid (theme-getx :color1)]]
+   [:.outer-cart {:border [[(u/px 1) :solid (get-theme-attribute :color1)]]
                   :border-radius (u/rem 0.4)}]
    [:.cart-title {:margin-left (u/em 1)
                   :margin-right (u/em 1)
@@ -249,7 +250,7 @@
    ;; TODO: Change naming of :color3? It is used as text color here,
    ;;   which means that it should have a good contrast with light background.
    ;;   This could be made explicit by changing the name accordingly.
-   [:.text-highlight {:color (theme-getx :color3)
+   [:.text-highlight {:color (get-theme-attribute :color3)
                       :font-weight "bold"}]))
 
 (defn- generate-form-group []
@@ -358,14 +359,14 @@
    [:a
     :button
     {:cursor :pointer
-     :color (theme-getx :link-color)}
-    [:&:hover {:color (theme-getx :link-hover-color :color4)}]]
+     :color (get-theme-attribute :link-color "#025b96")}
+    [:&:hover {:color (get-theme-attribute :link-hover-color :color4)}]]
    [:.pointer {:cursor :pointer}
     [:label.form-check-label {:cursor :pointer}]]
    [:html {:position :relative
            :min-width (u/px 320)
            :height (u/percent 100)}]
-   [:body {:font-family (theme-getx :font-family)
+   [:body {:font-family (get-theme-attribute :font-family "'Lato', sans-serif")
            :min-height (u/percent 100)
            :display :flex
            :flex-direction :column
@@ -382,8 +383,8 @@
                       :display :flex
                       :flex-direction :column}]
    [:.fixed-top {:background-color "#fff"
-                 :border-bottom (theme-getx :header-border)
-                 :box-shadow (theme-getx :header-shadow :table-shadow)
+                 :border-bottom (get-theme-attribute :header-border "3px solid #ccc")
+                 :box-shadow (get-theme-attribute :header-shadow :table-shadow)
                  :min-height menu-height}]
    [:.skip-navigation {:position :absolute
                        :left (u/em -1000)}
@@ -414,13 +415,13 @@
     [:&:hover
      :&:focus
      :&:active:hover
-     {:background-color (theme-getx :primary-button-hover-bgcolor :primary-button-bgcolor :color4)
-      :border-color (theme-getx :primary-button-hover-bgcolor :primary-button-bgcolor :color4)
-      :color (theme-getx :primary-button-hover-color :primary-button-color)
+     {:background-color (get-theme-attribute :primary-button-hover-bgcolor :primary-button-bgcolor :color4)
+      :border-color (get-theme-attribute :primary-button-hover-bgcolor :primary-button-bgcolor :color4)
+      :color (get-theme-attribute :primary-button-hover-color :primary-button-color "#fff")
       :outline-color :transparent}]
-    {:background-color (theme-getx :primary-button-bgcolor :color4)
-     :border-color (theme-getx :primary-button-bgcolor :color4)
-     :color (theme-getx :primary-button-color)
+    {:background-color (get-theme-attribute :primary-button-bgcolor :color4)
+     :border-color (get-theme-attribute :primary-button-bgcolor :color4)
+     :color (get-theme-attribute :primary-button-color "#fff")
      :outline-color :transparent}]
    [:.btn-secondary
     ;; Only override bootstrap's default if the key is defined in the theme
@@ -429,15 +430,15 @@
      :&:active:hover
      (into {}
            (filter val
-                   {:background-color (theme-getx :secondary-button-hover-bgcolor)
-                    :border-color (theme-getx :secondary-button-hover-bgcolor)
-                    :color (theme-getx :secondary-button-hover-color)
+                   {:background-color (get-theme-attribute :secondary-button-hover-bgcolor)
+                    :border-color (get-theme-attribute :secondary-button-hover-bgcolor)
+                    :color (get-theme-attribute :secondary-button-hover-color)
                     :outline-color :transparent}))]
     (into {}
           (filter val
-                  {:background-color (theme-getx :secondary-button-bgcolor)
-                   :border-color (theme-getx :secondary-button-bgcolor)
-                   :color (theme-getx :secondary-button-color)
+                  {:background-color (get-theme-attribute :secondary-button-bgcolor)
+                   :border-color (get-theme-attribute :secondary-button-bgcolor)
+                   :color (get-theme-attribute :secondary-button-color)
                    :outline-color :transparent}))]
    [:.btn-primary.disabled :.btn-primary:disabled ; same color as bootstrap's default for .btn-secondary.disabled
     {:color "#fff"
@@ -454,67 +455,64 @@
                      :padding "0.25em"
                      :text-align :center
                      :color "#ccc"}
-                    [:&:hover {:color (theme-getx :color4)
+                    [:&:hover {:color (get-theme-attribute :color4)
                                :background-color "#eee"}]]]
    [:.flash-message-title {:font-weight :bold}]
 
-   ;; TODO get rid of the text classes we don't use? At least -dark,
-   ;; -white, -light and -info seem unused currently.
-   [:.text-primary {:color (theme-getx :text-primary)}]
-   [:.text-secondary {:color (theme-getx :text-secondary)}]
-   [:.text-success {:color (theme-getx :text-success)}]
-   [:.text-danger {:color (theme-getx :text-danger)}]
-   [:.text-warning {:color (theme-getx :text-warning)}]
-   [:.text-info {:color (theme-getx :text-info)}]
-   [:.text-light {:color (theme-getx :text-light)}]
-   [:.text-dark {:color (theme-getx :text-dark)}]
-   [:.text-muted {:color (theme-getx :text-muted)}]
-   [:.text-white {:color (theme-getx :text-white)}]
-
-   [:.bg-primary {:background-color (theme-getx :bg-primary)}]
-   [:.bg-secondary {:background-color (theme-getx :bg-secondary)}]
-   [:.bg-success {:background-color (theme-getx :bg-success)}]
-   [:.bg-danger {:background-color (theme-getx :bg-danger)}]
-   [:.bg-warning {:background-color (theme-getx :bg-warning)}]
-   [:.bg-info {:background-color (theme-getx :bg-info)}]
-   [:.bg-light {:background-color (theme-getx :bg-light)}]
-   [:.bg-dark {:background-color (theme-getx :bg-dark)}]
-   [:.bg-white {:background-color (theme-getx :bg-white)}]
-
-   ;; TODO get rid of alert classes we don't use
-   [:.alert-primary {:color (theme-getx :alert-primary-color)
-                     :background-color (theme-getx :alert-primary-bgcolor)
-                     :border-color (theme-getx :alert-primary-bordercolor :alert-primary-color)}]
-   [:.alert-secondary {:color (theme-getx :alert-secondary-color)
-                       :background-color (theme-getx :alert-secondary-bgcolor)
-                       :border-color (theme-getx :alert-secondary-bordercolor :alert-secondary-color)}]
+   [:.text-primary {:color (get-theme-attribute :text-primary)}]
+   [:.text-secondary {:color (get-theme-attribute :text-secondary)}]
+   [:.text-success {:color (get-theme-attribute :text-success)}]
+   [:.text-danger {:color (get-theme-attribute :text-danger)}]
+   [:.text-warning {:color (get-theme-attribute :text-warning "#ffc107!important")}]
+   [:.text-info {:color (get-theme-attribute :text-info)}]
+   [:.text-light {:color (get-theme-attribute :text-light)}]
+   [:.text-dark {:color (get-theme-attribute :text-dark)}]
+   [:.text-muted {:color (get-theme-attribute :text-muted)}]
+   [:.text-white {:color (get-theme-attribute :text-white)}]
+
+   [:.bg-primary {:background-color (get-theme-attribute :bg-primary)}]
+   [:.bg-secondary {:background-color (get-theme-attribute :bg-secondary)}]
+   [:.bg-success {:background-color (get-theme-attribute :bg-success)}]
+   [:.bg-danger {:background-color (get-theme-attribute :bg-danger)}]
+   [:.bg-warning {:background-color (get-theme-attribute :bg-warning)}]
+   [:.bg-info {:background-color (get-theme-attribute :bg-info)}]
+   [:.bg-light {:background-color (get-theme-attribute :bg-light)}]
+   [:.bg-dark {:background-color (get-theme-attribute :bg-dark)}]
+   [:.bg-white {:background-color (get-theme-attribute :bg-white)}]
+
+   [:.alert-primary {:color (get-theme-attribute :alert-primary-color)
+                     :background-color (get-theme-attribute :alert-primary-bgcolor)
+                     :border-color (get-theme-attribute :alert-primary-bordercolor :alert-primary-color)}]
+   [:.alert-secondary {:color (get-theme-attribute :alert-secondary-color)
+                       :background-color (get-theme-attribute :alert-secondary-bgcolor)
+                       :border-color (get-theme-attribute :alert-secondary-bordercolor :alert-secondary-color)}]
    [:.alert-success
     (s/descendant :.state-approved.phases :.phase.completed)
     (s/descendant :.state-submitted.phases :.phase.completed)
-    {:color (theme-getx :alert-success-color)
-     :background-color (theme-getx :alert-success-bgcolor)
-     :border-color (theme-getx :alert-success-bordercolor :alert-success-color)}]
+    {:color (get-theme-attribute :alert-success-color)
+     :background-color (get-theme-attribute :alert-success-bgcolor)
+     :border-color (get-theme-attribute :alert-success-bordercolor :alert-success-color)}]
    [:.alert-danger
     :.state-rejected
     :.state-revoked
     (s/descendant :.state-rejected.phases :.phase.completed)
     (s/descendant :.state-revoked.phases :.phase.completed)
-    {:color (theme-getx :alert-danger-color)
-     :background-color (theme-getx :alert-danger-bgcolor)
-     :border-color (theme-getx :alert-danger-bordercolor :alert-danger-color)}]
-   [:.alert-warning {:color (theme-getx :alert-warning-color)
-                     :background-color (theme-getx :alert-warning-bgcolor)
-                     :border-color (theme-getx :alert-warning-bordercolor :alert-warning-color)}]
+    {:color (get-theme-attribute :alert-danger-color)
+     :background-color (get-theme-attribute :alert-danger-bgcolor)
+     :border-color (get-theme-attribute :alert-danger-bordercolor :alert-danger-color)}]
+   [:.alert-warning {:color (get-theme-attribute :alert-warning-color)
+                     :background-color (get-theme-attribute :alert-warning-bgcolor)
+                     :border-color (get-theme-attribute :alert-warning-bordercolor :alert-warning-color)}]
    [:.alert-info
-    {:color (theme-getx :alert-info-color)
-     :background-color (theme-getx :alert-info-bgcolor)
-     :border-color (theme-getx :alert-info-bordercolor :alert-info-color)}]
-   [:.alert-light {:color (theme-getx :alert-light-color)
-                   :background-color (theme-getx :alert-light-bgcolor)
-                   :border-color (theme-getx :alert-light-bordercolor :alert-light-color)}]
-   [:.alert-dark {:color (theme-getx :alert-dark-color)
-                  :background-color (theme-getx :alert-dark-bgcolor)
-                  :border-color (theme-getx :alert-dark-bordercolor :alert-dark-color)}]
+    {:color (get-theme-attribute :alert-info-color)
+     :background-color (get-theme-attribute :alert-info-bgcolor)
+     :border-color (get-theme-attribute :alert-info-bordercolor :alert-info-color)}]
+   [:.alert-light {:color (get-theme-attribute :alert-light-color)
+                   :background-color (get-theme-attribute :alert-light-bgcolor)
+                   :border-color (get-theme-attribute :alert-light-bordercolor :alert-light-color)}]
+   [:.alert-dark {:color (get-theme-attribute :alert-dark-color)
+                  :background-color (get-theme-attribute :alert-dark-bgcolor)
+                  :border-color (get-theme-attribute :alert-dark-bordercolor :alert-dark-color)}]
    shake
    [:.flash-message.alert-danger
     {:animation [[shake "0.6s cubic-bezier(.36,.07,.19,.97) both"]]}]
@@ -541,7 +539,7 @@
      :letter-spacing (u/rem 0.015)
      :padding-left 0
      :padding-right 0
-     :color (theme-getx :navbar-color)
+     :color (get-theme-attribute :navbar-color "#111")
      :justify-content "space-between"}
     [:.nav-link :.btn-link
      {:background-color :inherit}]]
@@ -553,25 +551,25 @@
                       :display :flex
                       :flex-direction :row}]
    [:.navbar-top-left {:flex 1
-                       :background-color (theme-getx :color4)}]
+                       :background-color (get-theme-attribute :color4)}]
    [:.navbar-top-right {:flex 1
-                        :background-color (theme-getx :color2)}]
+                        :background-color (get-theme-attribute :color2)}]
    [:.navbar-text {:font-size (u/px 19)
                    :font-weight (button-navbar-font-weight)}]
-   [:.navbar-toggler {:border-color (theme-getx :color1)}]
+   [:.navbar-toggler {:border-color (get-theme-attribute :color1)}]
    [:.nav-link
     :.btn-link
-    {:color (theme-getx :nav-color :link-color)
+    {:color (get-theme-attribute :nav-color :link-color :color3)
      :font-weight (button-navbar-font-weight)
      :border 0} ; for button links
     [:&.active
-     {:color (theme-getx :nav-active-color :color4)}]
+     {:color (get-theme-attribute :nav-active-color :color4)}]
     [:&:hover
-     {:color (theme-getx :nav-hover-color :color4)}]]
+     {:color (get-theme-attribute :nav-hover-color :color4)}]]
    [:.navbar {:white-space "nowrap"}]
    [(s/descendant :.user-widget :.nav-link) {:display :inline-block}]
    [:.user-name {:text-transform :none}]
-   [:#big-navbar {:text-transform (theme-getx :big-navbar-text-transform)}]
+   [:#big-navbar {:text-transform (get-theme-attribute :big-navbar-text-transform "none")}]
    [(s/descendant :.navbar-text :.language-switcher)
     {:margin-right (u/rem 1)}]
    [:.navbar-flex {:display "flex"
@@ -582,14 +580,14 @@
    ;; Logo, login, etc.
    (generate-logo-styles)
    ;; Footer
-   (let [footer-text-color (theme-getx :footer-color :table-heading-color)]
+   (let [footer-text-color (get-theme-attribute :footer-color :table-heading-color "#fff")]
      [:footer {:width "100%"
                :min-height (u/px 53.6)
                :color footer-text-color
                :font-size (u/px 19) ;; same as navbar
                :padding-top "1rem"
                :padding-bottom "1rem"
-               :background-color (theme-getx :footer-bgcolor :table-heading-bgcolor :color3)
+               :background-color (get-theme-attribute :footer-bgcolor :table-heading-bgcolor :color3)
                :margin-top (u/em 1)}
       [:a :a:hover {:color footer-text-color
                     :font-weight (button-navbar-font-weight)}]
@@ -604,7 +602,7 @@
      :margin-top (u/rem 2)
      :border-style "solid"
      :border-width (u/px 1)
-     :box-shadow (theme-getx :collapse-shadow :table-shadow)}
+     :box-shadow (get-theme-attribute :collapse-shadow :table-shadow)}
     [:h1 {:margin-bottom (u/px 20)}]]
    [:.login-btn {:max-height (u/px 70)
                  :margin-bottom (u/px 20)}
@@ -737,10 +735,10 @@
    [:.intro {:margin-bottom (u/rem 2)}]
    [:.rectangle {:width (u/px 50)
                  :height (u/px 50)}]
-   [:.color-1 {:background-color (theme-getx :color1)}]
-   [:.color-2 {:background-color (theme-getx :color2)}]
-   [:.color-3 {:background-color (theme-getx :color3)}]
-   [:.color-4 {:background-color (theme-getx :color4)}]
+   [:.color-1 {:background-color (get-theme-attribute :color1)}]
+   [:.color-2 {:background-color (get-theme-attribute :color2)}]
+   [:.color-3 {:background-color (get-theme-attribute :color3)}]
+   [:.color-4 {:background-color (get-theme-attribute :color4)}]
    [:.color-title {:padding-top (u/rem 0.8)}]
    [(s/descendant :.alert :ul) {:margin-bottom 0}]
    [:ul.comments {:list-style-type :none}]
@@ -751,8 +749,8 @@
                      :width "inherit"}]
    [:.clickable {:cursor :pointer}]
    [:.rems-card-margin-fix {:margin (u/px -1)}] ; make sure header overlaps container border
-   [:.rems-card-header {:color (theme-getx :table-heading-color)
-                        :background-color (theme-getx :table-heading-bgcolor :color3)}]
+   [:.rems-card-header {:color (get-theme-attribute :table-heading-color "#fff")
+                        :background-color (get-theme-attribute :table-heading-bgcolor :color3)}]
    [(s/descendant :.card-header :a) {:color :inherit}]
    [:.application-resources
     [:.application-resource {:margin-bottom (u/rem 1)
@@ -771,24 +769,25 @@
    [:.collapse-toggle {:text-align :center}]
    [:.collapse-wrapper {:border-radius (u/rem 0.4)
                         :border "1px solid #ccc"
-                        :background-color (theme-getx :collapse-bgcolor)
-                        :box-shadow (theme-getx :collapse-shadow :table-shadow)}
+                        :background-color (get-theme-attribute :collapse-bgcolor "#fff")
+                        :box-shadow (get-theme-attribute :collapse-shadow :table-shadow)}
     [:.card-header {:border-bottom "none"
                     :border-radius (u/rem 0.4)
                     :font-weight 400
                     :font-size (u/rem 1.5)
                     :line-height 1.1
-                    :font-family (theme-getx :font-family)
-                    :color (theme-getx :collapse-color)}]]
+                    :font-family (get-theme-attribute :font-family "'Lato', sans-serif")
+                    :color (get-theme-attribute :collapse-color "#fff")}
+     [:&.alert-danger {:color (get-theme-attribute :danger-color "inherit")}]]]
    [:.collapse-content {:margin (u/rem 1.25)}]
    [:.collapse-wrapper.slow
     [:.collapsing {:-webkit-transition "height 0.25s linear"
                    :-o-transition "height 0.25s linear"
                    :transition "height 0.25s linear"}]]
 
-   [:.color1 {:color (theme-getx :color1)}]
-   [:.color1-faint {:color (when (theme-getx :color1)
-                             (-> (theme-getx :color1)
+   [:.color1 {:color (get-theme-attribute :color1)}]
+   [:.color1-faint {:color (when (get-theme-attribute :color1)
+                             (-> (get-theme-attribute :color1)
                                  (c/saturate -50)
                                  (c/lighten 33)))}]
    [:h2 {:margin [[(u/rem 3) 0 (u/rem 1) 0]]}]
@@ -830,8 +829,8 @@
    ;; by more specific styles by react-select.
    [:.dropdown-select__option--is-focused
     (make-important
-     {:color (theme-getx :table-heading-color)
-      :background-color (theme-getx :table-heading-bgcolor :color3)})]
+     {:color (get-theme-attribute :table-heading-color "#fff")
+      :background-color (get-theme-attribute :table-heading-bgcolor :color3)})]
    [:.dropdown-select__control--is-focused
     (make-important
      {:color "#495057"
@@ -888,6 +887,10 @@
 (defn screen-css []
   (g/css {:pretty-print? false} (remove-nil-vals (build-screen))))
 
+(deftest test-screen-css
+  (binding [context/*lang* :fi]
+    (is (string? (screen-css)))))
+
 ;; For development use and Figwheel updates render all configured CSS
 ;; files so that Figwheel will notice this change and force our app
 ;; to reload CSS files from the usual route.
diff --git a/src/clj/rems/email/template.clj b/src/clj/rems/email/template.clj
index 6aab91fa5..ec4f1686d 100644
--- a/src/clj/rems/email/template.clj
+++ b/src/clj/rems/email/template.clj
@@ -236,16 +236,6 @@
                (text :t.email/regards)
                (text :t.email/footer))}])))
 
-(defmethod event-to-emails :application.event/applicant-changed [event application]
-  (concat (emails-to-recipients (application-util/applicant-and-members application)
-                                event application
-                                :t.email.applicant-changed/subject-to-member
-                                :t.email.applicant-changed/message-to-member)
-          (emails-to-recipients (other-handlers event application)
-                                event application
-                                :t.email.applicant-changed/subject-to-handler
-                                :t.email.applicant-changed/message-to-handler)))
-
 ;; TODO member-joined?
 
 (defn handler-reminder-email [lang handler applications]
diff --git a/src/clj/rems/form_validation.clj b/src/clj/rems/form_validation.clj
index d078cb116..cdd4b9d30 100644
--- a/src/clj/rems/form_validation.clj
+++ b/src/clj/rems/form_validation.clj
@@ -85,16 +85,6 @@
       {:field-id (:field/id field)
        :type     :t.form.validation/invalid-value})))
 
-(defn- multiselect-value-valid? [field]
-  (let [allowed? (set (conj (map :key (:field/options field)) ""))]
-    (every? allowed? (form/parse-multiselect-values (:field/value field)))))
-
-(defn- invalid-multiselect-error [field]
-  (when (= (:field/type field) :multiselect)
-    (when-not (multiselect-value-valid? field)
-      {:field-id (:field/id field)
-       :type     :t.form.validation/invalid-value})))
-
 (defn- missing-columns-error [field]
   (when (= (:field/type field) :table)
     (let [columns (set (map :key (:field/columns field)))
@@ -136,7 +126,6 @@
       (invalid-ip-address-error field)
       (too-long-error field)
       (invalid-option-error field)
-      (invalid-multiselect-error field)
       (missing-columns-error field)
       (invalid-attachment-error field)))
 
diff --git a/src/cljc/rems/common/form.cljc b/src/cljc/rems/common/form.cljc
index fde371a68..dafd356e3 100644
--- a/src/cljc/rems/common/form.cljc
+++ b/src/cljc/rems/common/form.cljc
@@ -159,25 +159,8 @@
            (not (<= (first max-length-range) parsed (second max-length-range)))
            :t.form.validation/invalid-value))})
 
-(defn normalize-option-key
-  "Strips disallowed characters from an option key"
-  [key]
-  (str/replace key #"\s+" ""))
-
-(deftest test-normalize-option-key
-  (is (= "foo" (normalize-option-key " f o o "))))
-
-(defn- validate-key [option]
-  (let [val (get option :key)]
-    (cond
-      (str/blank? val)
-      {:key :t.form.validation/required}
-
-      (not= val (normalize-option-key val))
-      {:key :t.form.validation/invalid-value})))
-
 (defn- validate-option [option id languages]
-  {id (merge (validate-key option)
+  {id (merge (validate-text-field option :key)
              (validate-localized-text-field option :label languages))})
 
 (defn- validate-options [options languages]
@@ -441,10 +424,6 @@
                  (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :key] "") languages)
                  (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :key] nil) languages))))
 
-        (testing "invalid option key"
-          (is (= {:form/fields {0 {:field/options {0 {:key :t.form.validation/invalid-value}}}}}
-                 (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :key] "e gg") languages))))
-
         (testing "missing option label"
           (let [empty-label (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :label] {:en "" :fi ""}) languages)
                 nil-label (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :label] nil) languages)]
@@ -477,10 +456,6 @@
                  (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :key] "") languages)
                  (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :key] nil) languages))))
 
-        (testing "invalid option key"
-          (is (= {:form/fields {0 {:field/options {0 {:key :t.form.validation/invalid-value}}}}}
-                 (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :key] "e gg") languages))))
-
         (testing "missing option label"
           (let [empty-label (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :label] {:en "" :fi ""}) languages)
                 nil-label (validate-form-template (assoc-in form [:form/fields 0 :field/options 0 :label] nil) languages)]
@@ -545,11 +520,7 @@
         (testing "missing column key"
           (is (= {:form/fields {0 {:field/columns {0 {:key :t.form.validation/required}}}}}
                  (validate-form-template (assoc-in form [:form/fields 0 :field/columns 0 :key] nil) languages)
-                 (validate-form-template (update-in form [:form/fields 0 :field/columns 0] dissoc :key) languages))))
-
-        (testing "invalid option key"
-          (is (= {:form/fields {0 {:field/columns {0 {:key :t.form.validation/invalid-value}}}}}
-                 (validate-form-template (assoc-in form [:form/fields 0 :field/columns 0 :key] "col 1") languages))))))
+                 (validate-form-template (update-in form [:form/fields 0 :field/columns 0] dissoc :key) languages))))))
 
     (testing "visible"
       (let [form (assoc form :form/fields
@@ -625,30 +596,3 @@
   (is (= "1" (unparse-attachment-ids [1])))
   (is (= [1 236 3] (parse-attachment-ids "1,236,3")))
   (is (=  "1,236,3" (unparse-attachment-ids [1 236 3]))))
-
-;; TODO for historical reasons we separate multiselect values with a
-;; space, and attachments with a comma.
-
-(defn unparse-multiselect-values
-  "Encodes a set of option keys to a string"
-  [keys]
-  (->> keys
-       sort
-       (str/join " ")))
-
-(defn parse-multiselect-values
-  "Decodes a set of option keys from a string"
-  [value]
-  (-> value
-      (str/split #"\s+")
-      set
-      (disj "")))
-
-(deftest test-parse-unparse-multiselect-values
-  (is (= #{} (parse-multiselect-values "")))
-  (is (= "" (unparse-multiselect-values nil)))
-  (is (= "" (unparse-multiselect-values #{})))
-  (is (= #{"yes"} (parse-multiselect-values "yes")))
-  (is (= "yes" (unparse-multiselect-values #{"yes"})))
-  (is (= #{"yes" "maybe" "no"} (parse-multiselect-values "yes maybe no")))
-  (is (= "maybe no yes" (unparse-multiselect-values #{"yes" "maybe" "no"}))))
diff --git a/src/cljc/rems/text.cljc b/src/cljc/rems/text.cljc
index facd73e01..f67668121 100644
--- a/src/cljc/rems/text.cljc
+++ b/src/cljc/rems/text.cljc
@@ -118,8 +118,7 @@
 
 
 (def ^:private event-types
-  {:application.event/applicant-changed :t.applications.events/applicant-changed
-   :application.event/approved :t.applications.events/approved
+  {:application.event/approved :t.applications.events/approved
    :application.event/closed :t.applications.events/closed
    :application.event/review-requested :t.applications.events/review-requested
    :application.event/reviewed :t.applications.events/reviewed
@@ -186,9 +185,6 @@
          :application.event/member-removed)
         (application-util/get-member-name (:application/member event))
 
-        :application.event/applicant-changed
-        (application-util/get-member-name (:application/applicant event))
-
         (:application.event/member-invited
          :application.event/member-uninvited)
         (localize-invitation (:application/member event))
diff --git a/src/cljs/rems/actions/change_applicant.cljs b/src/cljs/rems/actions/change_applicant.cljs
deleted file mode 100644
index a8a06dbf2..000000000
--- a/src/cljs/rems/actions/change_applicant.cljs
+++ /dev/null
@@ -1,62 +0,0 @@
-(ns rems.actions.change-applicant
-  (:require [re-frame.core :as rf]
-            [rems.actions.components :refer [action-button action-form-view comment-field button-wrapper collapse-action-form]]
-            [rems.flash-message :as flash-message]
-            [rems.text :refer [text]]
-            [rems.util :refer [post!]]))
-
-(rf/reg-event-fx
- ::reset-form
- (fn [_ [_ element-id]]
-   {:dispatch [:rems.actions.components/set-comment (str element-id "-comment") ""]}))
-
-;; The API allows us to add attachments to these commands
-;; but this is left out from the UI for simplicity
-(rf/reg-event-fx
- ::change-applicant
- (fn [_ [_ {:keys [collapse-id application-id member comment on-finished]}]]
-   (let [description [text :t.actions/change-applicant]]
-     (post! "/api/applications/change-applicant"
-            {:params {:application-id application-id
-                      :member (select-keys member [:userid])
-                      :comment comment}
-             :handler (flash-message/default-success-handler
-                       :change-members
-                       description
-                       (fn [_]
-                         (collapse-action-form collapse-id)
-                         (on-finished)))
-             :error-handler (flash-message/default-error-handler :change-members description)}))
-   {}))
-
-(defn- qualify-parent-id [parent-id]
-  (str parent-id "-change"))
-
-(defn change-applicant-action-button [parent-id]
-  (let [element-id (qualify-parent-id parent-id)]
-    [action-button {:id element-id
-                    :text (text :t.actions/change-applicant)
-                    :on-click #(rf/dispatch [::reset-form element-id])}]))
-
-(defn- change-applicant-view
-  [{:keys [parent-id on-send]}]
-  (let [element-id (qualify-parent-id parent-id)]
-    [action-form-view element-id
-     (text :t.actions/change-applicant)
-     [[button-wrapper {:id (str element-id "-submit")
-                       :text (text :t.actions/change-applicant)
-                       :class "btn-primary"
-                       :on-click on-send}]]
-     [comment-field {:field-key (str element-id "-comment")
-                     :label (text :t.form/add-comments-shown-to-applicant)}]
-     {:collapse-id parent-id}]))
-
-(defn change-applicant-form [parent-id member application-id on-finished]
-  (let [element-id (qualify-parent-id parent-id)
-        comment @(rf/subscribe [:rems.actions.components/comment (str element-id "-comment")])]
-    [change-applicant-view {:parent-id parent-id
-                            :on-send #(rf/dispatch [::change-applicant {:application-id application-id
-                                                                        :collapse-id element-id
-                                                                        :comment comment
-                                                                        :member member
-                                                                        :on-finished on-finished}])}]))
diff --git a/src/cljs/rems/actions/remove_member.cljs b/src/cljs/rems/actions/remove_member.cljs
index 0616e3af8..13c9805f9 100644
--- a/src/cljs/rems/actions/remove_member.cljs
+++ b/src/cljs/rems/actions/remove_member.cljs
@@ -34,34 +34,28 @@
              :error-handler (flash-message/default-error-handler :change-members description)}))
    {}))
 
-(defn- qualify-parent-id [parent-id]
-  (str parent-id "-remove"))
-
-(defn remove-member-action-button [parent-id]
-  (let [element-id (qualify-parent-id parent-id)]
-    [action-button {:id element-id
-                    :text (text :t.actions/remove-member)
-                    :on-click #(rf/dispatch [::reset-form element-id])}]))
+(defn remove-member-action-button [element-id]
+  [action-button {:id (str element-id "-form")
+                  :text (text :t.actions/remove-member)
+                  :on-click #(rf/dispatch [::reset-form element-id])}])
 
 (defn- remove-member-view
-  [{:keys [parent-id on-send]}]
-  (let [element-id (qualify-parent-id parent-id)]
-    [action-form-view element-id
-     (text :t.actions/remove-member)
-     [[button-wrapper {:id (str element-id "-submit")
-                       :text (text :t.actions/remove-member)
-                       :class "btn-primary"
-                       :on-click on-send}]]
-     [comment-field {:field-key (str element-id "-comment")
-                     :label (text :t.form/add-comments-shown-to-applicant)}]
-     {:collapse-id parent-id}]))
+  [{:keys [element-id on-send]}]
+  [action-form-view (str element-id "-form")
+   (text :t.actions/remove-member)
+   [[button-wrapper {:id (str element-id "-submit")
+                     :text (text :t.actions/remove-member)
+                     :class "btn-primary"
+                     :on-click on-send}]]
+   [comment-field {:field-key (str element-id "-comment")
+                   :label (text :t.form/add-comments-shown-to-applicant)}]
+   {:collapse-id element-id}])
 
-(defn remove-member-form [parent-id member application-id on-finished]
-  (let [element-id (qualify-parent-id parent-id)
-        comment @(rf/subscribe [:rems.actions.components/comment (str element-id "-comment")])]
-    [remove-member-view {:parent-id parent-id
+(defn remove-member-form [element-id member application-id on-finished]
+  (let [comment @(rf/subscribe [:rems.actions.components/comment (str element-id "-comment")])]
+    [remove-member-view {:element-id element-id
                          :on-send #(rf/dispatch [::remove-member {:application-id application-id
-                                                                  :collapse-id element-id
+                                                                  :collapse-id (str element-id "-form")
                                                                   :comment comment
                                                                   :member member
                                                                   :on-finished on-finished}])}]))
diff --git a/src/cljs/rems/administration/create_form.cljs b/src/cljs/rems/administration/create_form.cljs
index 3348d1a2d..1bc83eac7 100644
--- a/src/cljs/rems/administration/create_form.cljs
+++ b/src/cljs/rems/administration/create_form.cljs
@@ -12,6 +12,7 @@
   React to re-render it. So we want to wait until the element is rendered to the new place
   with the new index before we can scroll to the new position."
   (:require [clojure.string :as str]
+            [goog.string :refer [parseInt]]
             [medley.core :refer [find-first]]
             [re-frame.core :as rf]
             [rems.administration.administration :as administration]
@@ -21,14 +22,14 @@
             [rems.collapsible :as collapsible]
             [rems.common.form :refer [field-visible? generate-field-id validate-form-template] :as common-form]
             [rems.common.util :refer [parse-int]]
-            [rems.dropdown :as dropdown]
             [rems.fetcher :as fetcher]
             [rems.fields :as fields]
             [rems.flash-message :as flash-message]
             [rems.focus :as focus]
+            [rems.common.roles :as roles]
             [rems.spinner :as spinner]
             [rems.text :refer [text text-format]]
-            [rems.util :refer [navigate! put! post! trim-when-string visibility-ratio focus-input-field]]))
+            [rems.util :refer [navigate! fetch put! post! normalize-option-key trim-when-string visibility-ratio focus-input-field]]))
 
 (rf/reg-event-fx
  ::enter-page
@@ -330,7 +331,7 @@
      [remove-form-field-option-button field-index option-index]]]
    [text-field-inline context {:keys [:form/fields field-index :field/options option-index :key]
                                :label (text :t.create-form/option-key)
-                               :normalizer common-form/normalize-option-key}]
+                               :normalizer normalize-option-key}]
    [localized-text-field context {:keys [:form/fields field-index :field/options option-index :label]
                                   :label (text :t.create-form/option-label)}]])
 
@@ -372,7 +373,7 @@
      [remove-form-field-column-button field-index column-index]]]
    [text-field-inline context {:keys [:form/fields field-index :field/columns column-index :key]
                                :label (text :t.create-form/column-key)
-                               :normalizer common-form/normalize-option-key}]
+                               :normalizer normalize-option-key}]
    [localized-text-field context {:keys [:form/fields field-index :field/columns column-index :label]
                                   :label (text :t.create-form/column-label)}]])
 
@@ -409,7 +410,7 @@
    (assoc-in db [::form :data :form/fields field-index :field/visibility :visibility/field] visibility-field)))
 
 (rf/reg-event-db
- ::form-field-visibility-values
+ ::form-field-visibility-value
  (fn [db [_ field-index visibility-value]]
    (assoc-in db [::form :data :form/fields field-index :field/visibility :visibility/values] visibility-value)))
 
@@ -434,8 +435,8 @@
         visibility-value (:visibility/values visibility)]
     [:div {:class (when (= :only-if visibility-type) "form-field-visibility")}
      [:div.form-group.field.row {:id (str "container-field" field-index)}
-      [:label.col-sm-3.col-form-label {:for id-type} label-type]
-      [:div.col-sm-9
+      [:label.col-sm-2.col-form-label {:for id-type} label-type]
+      [:div.col-sm-10
        [:select.form-control
         {:id id-type
          :class (when error-type "is-invalid")
@@ -448,8 +449,8 @@
      (when (= :only-if visibility-type)
        [:<>
         [:div.form-group.field.row
-         [:label.col-sm-3.col-form-label {:for id-field} label-field]
-         [:div.col-sm-9
+         [:label.col-sm-2.col-form-label {:for id-field} label-field]
+         [:div.col-sm-10
           [:select.form-control
            {:id id-field
             :class (when error-field "is-invalid")
@@ -465,19 +466,18 @@
            (when error-field (text-format error-field label-field))]]]
         (when (:field/id visibility-field)
           [:div.form-group.field.row
-           [:label.col-sm-3.col-form-label {:for id-value} label-value]
-           [:div.col-sm-9
-            [dropdown/dropdown
+           [:label.col-sm-2.col-form-label {:for id-value} label-value]
+           [:div.col-sm-10
+            [:select.form-control
              {:id id-value
-              :items (for [value (form-field-values form (:field/id visibility-field))]
-                       {:value (:value value)
-                        :label (get-in value [:title lang])})
-              :item-key #(str field-index "-" %)
-              :item-label :label
-              :item-selected? #(contains? (set visibility-value) (:value %))
-              :multi? true
-              :clearable? true
-              :on-change #(rf/dispatch [::form-field-visibility-values field-index (mapv :value %)])}]
+              :class (when error-value "is-invalid")
+              :on-change #(rf/dispatch [::form-field-visibility-value field-index [(.. % -target -value)]])
+              :value (or (first visibility-value) "")}
+             ^{:key "not-selected"} [:option ""]
+             (doall
+              (for [value (form-field-values form (:field/id visibility-field))]
+                ^{:key (str field-index "-" (:value value))}
+                [:option {:value (:value value)} (get-in value [:title lang])]))]
             [:div.invalid-feedback
              (when error-value (text-format error-value label-value))]]])])]))
 
diff --git a/src/cljs/rems/application.cljs b/src/cljs/rems/application.cljs
index 4ee0679ca..8767ffcba 100644
--- a/src/cljs/rems/application.cljs
+++ b/src/cljs/rems/application.cljs
@@ -8,7 +8,6 @@
             [rems.actions.add-member :refer [add-member-action-button add-member-form]]
             [rems.actions.approve-reject :refer [approve-reject-action-button approve-reject-form]]
             [rems.actions.assign-external-id :refer [assign-external-id-button assign-external-id-form]]
-            [rems.actions.change-applicant :refer [change-applicant-action-button change-applicant-form]]
             [rems.actions.change-resources :refer [change-resources-action-button change-resources-form]]
             [rems.actions.close :refer [close-action-button close-form]]
             [rems.actions.decide :refer [decide-action-button decide-form]]
@@ -322,7 +321,7 @@
 (defn- attachment-zip-button [application]
   (when-not (empty? (:application/attachments application))
     [:a.btn.btn-secondary
-     {:href (str "/api/applications/" (:application/id application) "/attachments?all=false")
+     {:href (str "/api/applications/" (:application/id application) "/attachments")
       :target :_blank}
      [file-download] " " (text :t.form/attachments-as-zip)]))
 
@@ -618,43 +617,31 @@
   `:element-id`         - id of the element to generate unique ids
   `:attributes`         - user attributes to display
   `:application`        - application
-  `:group?`             - specifies if a group border is rendered"
-  [{:keys [element-id attributes application group?]}]
+  `:group?`             - specifies if a group border is rendered
+  `:can-remove?`        - can the user be removed?
+  `:accepted-licenses?` - has the member accepted the licenses?"
+  [{:keys [element-id attributes application group? can-remove? accepted-licenses?]}]
   (let [application-id (:application/id application)
         user-id (:userid attributes)
-        invited-user? (nil? user-id)
-        applicant? (= (:userid (:application/applicant application)) user-id)
-        title (cond applicant? (text :t.applicant-info/applicant)
-                    invited-user? (text :t.applicant-info/invited-member)
-                    :else (text :t.applicant-info/member))
-        accepted? (accepted-licenses? application user-id)
-        permissions (:application/permissions application)
-        can-remove? (and (not applicant?)
-                         (contains? permissions :application.command/remove-member))
-        can-uninvite? (and invited-user?
-                           (contains? permissions :application.command/uninvite-member))
-        can-change? (and (not applicant?)
-                         (not invited-user?)
-                         (contains? permissions :application.command/change-applicant))]
+        title (cond (= (:userid (:application/applicant application)) user-id) (text :t.applicant-info/applicant)
+                    user-id (text :t.applicant-info/member)
+                    :else (text :t.applicant-info/invited-member))]
     [collapsible/minimal
      {:id (str element-id "-info")
       :class (when group? "group")
       :always [:div
                [:h3 title]
                [user/username attributes]
-               (when-not (or invited-user?
-                             (= :application.state/draft (:application/state application)))
-                 [info-field (text :t.form/accepted-licenses) [readonly-checkbox {:value accepted?}] {:inline? true}])]
-      :collapse [user/attributes attributes invited-user?]
-      :footer (let [element-id (str element-id "-operations")]
+               (when-not (nil? accepted-licenses?)
+                 [info-field (text :t.form/accepted-licenses) [readonly-checkbox {:value accepted-licenses?}] {:inline? true}])]
+      :collapse [user/attributes attributes]
+      :footer (let [element-id (str element-id "-remove-member")]
                 [:div {:id element-id}
-                 [:div.commands
-                  (when can-change?
-                    [change-applicant-action-button element-id])
-                  (when (or can-remove? can-uninvite?)
-                    [remove-member-action-button element-id])]
-                 [change-applicant-form element-id attributes application-id (partial reload! application-id)]
-                 [remove-member-form element-id attributes application-id (partial reload! application-id)]])}]))
+                 (when can-remove?
+                   [:div.commands
+                    [remove-member-action-button element-id]])
+                 (when can-remove?
+                   [remove-member-form element-id attributes application-id (partial reload! application-id)])])}]))
 
 (defn applicants-info
   "Renders the applicants, i.e. applicant and members."
@@ -665,7 +652,9 @@
         invited-members (:application/invited-members application)
         permissions (:application/permissions application)
         can-add? (contains? permissions :application.command/add-member)
-        can-invite? (contains? permissions :application.command/invite-member)]
+        can-remove? (contains? permissions :application.command/remove-member)
+        can-invite? (contains? permissions :application.command/invite-member)
+        can-uninvite? (contains? permissions :application.command/uninvite-member)]
     [collapsible/component
      {:id "applicants-info"
       :title (text :t.applicant-info/applicants)
@@ -676,18 +665,24 @@
                            :attributes applicant
                            :application application
                            :group? (or (seq members)
-                                       (seq invited-members))}]]
+                                       (seq invited-members))
+                           :can-remove? false
+                           :accepted-licenses? (when (not= :application.state/draft (:application/state application))
+                                                 (accepted-licenses? application (:userid applicant)))}]]
             (concat
              (for [[index member] (map-indexed vector (sort-by :name members))]
                [member-info {:element-id (str "member" index)
                              :attributes member
                              :application application
-                             :group? true}])
+                             :group? true
+                             :can-remove? can-remove?
+                             :accepted-licenses? (accepted-licenses? application (:userid member))}])
              (for [[index invited-member] (map-indexed vector (sort-by :name invited-members))]
                [member-info {:element-id (str "invite" index)
                              :attributes invited-member
                              :application application
-                             :group? true}])))
+                             :group? true
+                             :can-remove? can-uninvite?}])))
       :footer [:div
                [:div.commands
                 (when can-invite? [invite-member-action-button])
@@ -875,7 +870,7 @@
 (defn guide []
   [:div
    (component-info member-info)
-   (example "member-info: applicant with notification email, accepted licenses, researcher status"
+   (example "member-info"
             [member-info {:element-id "info1"
                           :attributes {:userid "developer@uu.id"
                                        :email "developer@uu.id"
@@ -885,37 +880,29 @@
                                        :address "Testikatu 1, 00100 Helsinki"
                                        :researcher-status-by "so"}
                           :application {:application/id 42
-                                        :application/applicant {:userid "developer@uu.id"}
-                                        :application/licenses [{:license/id 1}]
-                                        :application/accepted-licenses {"developer@uu.id" #{1}}}}])
+                                        :application/applicant {:userid "developer"}}
+                          :accepted-licenses? true}])
    (example "member-info with name missing"
             [member-info {:element-id "info2"
                           :attributes {:userid "developer"
                                        :email "developer@uu.id"
-                                       :address "Testikatu 1, 00100 Helsinki"}}])
-   (example "member-info with buttons, licenses not accepted"
+                                       :name "Testers"
+                                       :address "Testikatu 1, 00100 Helsinki"}
+                          :application {:application/id 42
+                                        :application/applicant {:userid "developer"}}}])
+   (example "member-info"
             [member-info {:element-id "info3"
                           :attributes {:userid "alice"}
-                          :application {:application/id 42
-                                        :application/applicant {:userid "developer"}
-                                        :application/licenses [{:license/id 1}]
-                                        :application/permissions #{:application.command/remove-member
-                                                                   :application.command/change-applicant}}
-                          :group? true}])
-   (example "member-info: invited member"
-            [member-info {:element-id "info4"
-                          :attributes {:name "John Smith"
-                                       :email "john.smith@invited.com"}
                           :application {:application/id 42
                                         :application/applicant {:userid "developer"}}
-                          :group? true}])
-   (example "member-info: invited member, with remove button"
+                          :group? true
+                          :can-remove? true}])
+   (example "member-info"
             [member-info {:element-id "info4"
                           :attributes {:name "John Smith"
                                        :email "john.smith@invited.com"}
                           :application {:application/id 42
-                                        :application/applicant {:userid "developer"}
-                                        :application/permissions #{:application.command/uninvite-member}}
+                                        :application/applicant {:userid "developer"}}
                           :group? true}])
 
    (component-info applicants-info)
diff --git a/src/cljs/rems/fields.cljs b/src/cljs/rems/fields.cljs
index ad0ea3ae9..c489bc998 100644
--- a/src/cljs/rems/fields.cljs
+++ b/src/cljs/rems/fields.cljs
@@ -4,12 +4,11 @@
             [rems.administration.items :as items]
             [rems.atoms :refer [add-symbol attachment-link close-symbol failure-symbol success-symbol textarea]]
             [rems.common.attachment-types :as attachment-types]
-            [rems.common.form :as common-form]
             [rems.common.util :refer [build-index getx]]
             [rems.guide-util :refer [component-info example lipsum-short lipsum-paragraphs]]
             [rems.spinner :as spinner]
             [rems.text :refer [localized text text-format]]
-            [rems.util :refer [focus-when-collapse-opened linkify]]))
+            [rems.util :refer [encode-option-keys decode-option-keys focus-when-collapse-opened linkify]]))
 
 (defn field-name [field]
   (str "form-" (getx field :form/id) "-field-" (getx field :field/id)))
@@ -272,7 +271,7 @@
   (let [value (:field/value opts)
         options (:field/options opts)
         optional (:field-/optional opts)
-        selected-keys (common-form/parse-multiselect-values value)]
+        selected-keys (decode-option-keys value)]
     [field-wrapper
      (assoc opts
             :fieldset true
@@ -289,7 +288,7 @@
                                      selected-keys (if checked
                                                      (conj selected-keys key)
                                                      (disj selected-keys key))]
-                                 (on-change (common-form/unparse-multiselect-values selected-keys))))]
+                                 (on-change (encode-option-keys selected-keys))))]
                [:div.form-check
                 [:input.form-check-input {:type "checkbox"
                                           :id option-id
diff --git a/src/cljs/rems/profile.cljs b/src/cljs/rems/profile.cljs
index a365c951f..8aae72720 100644
--- a/src/cljs/rems/profile.cljs
+++ b/src/cljs/rems/profile.cljs
@@ -100,7 +100,7 @@
        {:title (text :t.profile/your-details)
         :always [:<>
                  [user/username (:user identity)]
-                 [user/attributes attributes false]]}]]]))
+                 [user/attributes (:user identity)]]}]]]))
 
 (defn guide []
   [:div
diff --git a/src/cljs/rems/user.cljs b/src/cljs/rems/user.cljs
index 3002c85ff..636ec1ccf 100644
--- a/src/cljs/rems/user.cljs
+++ b/src/cljs/rems/user.cljs
@@ -14,11 +14,8 @@
     [info-field (text :t.applicant-info/name) name {:inline? true}]))
 
 (defn attributes
-  "A div with a rems.atoms/info-field for every user attribute in the given attributes.
-   Accepts 
-    - attributes: map, user attributes
-    - invited-user? : boolean, if user is invited, shows different email string"
-  [attributes invited-user?]
+  "A div with a rems.atoms/info-field for every user attribute in the given attributes."
+  [attributes]
   (let [language @(rf/subscribe [:language])
         organization-by-id @(rf/subscribe [:organization-by-id])
         organization-name-if-known (fn [organization]
@@ -33,9 +30,7 @@
            (when-let [mail (:notification-email attributes)]
              [info-field (text :t.applicant-info/notification-email) mail {:inline? true}])
            (when-let [mail (:email attributes)]
-             (if invited-user?
-               [info-field (text :t.applicant-info/email) mail {:inline? true}]
-               [info-field (text :t.applicant-info/email-idp) mail {:inline? true}]))
+             [info-field (text :t.applicant-info/email) mail {:inline? true}])
            (when-let [organizations (seq (:organizations attributes))]
              [info-field (text :t.applicant-info/organization) (str/join ", " (map organization-name-if-known organizations)) {:inline? true}])
            (when (#{"so" "system"} (:researcher-status-by attributes))
@@ -58,7 +53,7 @@
    (example "empty attributes"
             [username {}])
    (component-info attributes)
-   (example "full set of attributes, false invited-user status"
+   (example "full set of attributes"
             [attributes {:userid "developer@uu.id"
                          :email "developer@uu.id"
                          :name "Deve Loper"
@@ -66,22 +61,14 @@
                          :organizations [{:organization/id "Testers"} {:organization/id "Users"}]
                          :address "Testikatu 1, 00100 Helsinki"
                          :researcher-status-by "so"
-                         :nickname "The Dev"}
-             false])
-   (example "invited memeber set of attributes, true invited-user status"
-            [attributes {:userid "invited@memeber.com"
-                         :email "invited@memeber.com"
-                         :name "Invited Mamber"
-                         :notification-email "invited@memeber.com"
-                         :organizations []}
-             true])
-   (example "invalid value for researcher status, no invited-user status"
+                         :nickname "The Dev"}])
+   (example "invalid value for researcher status"
             [attributes {:userid "developer@uu.id"
                          :email "developer@uu.id"
                          :organizations [{:organization/id "Testers"} {:organization/id "Users"}]
                          :researcher-status-by :dac}])
-   (example "less attributes, no invited-user status"
+   (example "less attributes"
             [attributes {:email "developer@uu.id"
                          :organizations [{:organization/id "Testers"} {:organization/id "Users"}]}])
-   (example "empty attributes, no invited-user status"
+   (example "empty attributes"
             [attributes {}])])
diff --git a/src/cljs/rems/util.cljs b/src/cljs/rems/util.cljs
index 683c1705d..80171cf5e 100644
--- a/src/cljs/rems/util.cljs
+++ b/src/cljs/rems/util.cljs
@@ -77,7 +77,7 @@
 
   Has sensible defaults with error handler, JSON and keywords.
   You can use :custom-error-handler? to decide weather you would like use wrapper for the error handling.
-
+  
   Additionally calls event hooks.
 
   Returns a promise, but it's okay to ignore it if you prefer using
@@ -126,6 +126,26 @@
 (defn trim-when-string [s]
   (when (string? s) (str/trim s)))
 
+(defn normalize-option-key
+  "Strips disallowed characters from an option key"
+  [key]
+  (str/replace key #"\s+" ""))
+
+(defn encode-option-keys
+  "Encodes a set of option keys to a string"
+  [keys]
+  (->> keys
+       sort
+       (str/join " ")))
+
+(defn decode-option-keys
+  "Decodes a set of option keys from a string"
+  [value]
+  (-> value
+      (str/split #"\s+")
+      set
+      (disj "")))
+
 (def ^:private link-regex #"(?:http://|https://|www\.\w).*?(?=[^a-zA-Z0-9_/]*(?: |$))")
 
 (defn linkify


Test patch:
diff --git a/src/clj/rems/test_styles.clj b/src/clj/rems/test_styles.clj
deleted file mode 100644
index 9b8afc078..000000000
--- a/src/clj/rems/test_styles.clj
+++ /dev/null
@@ -1,18 +0,0 @@
-(ns ^:integration rems.test-styles
-  (:require [clojure.test :refer :all]
-            [mount.core :as mount]
-            [rems.config]
-            [rems.context :as context]
-            [rems.css.styles :as styles]))
-
-
-(use-fixtures
-  :once
-  (fn [f]
-    (mount/start #'rems.config/env)
-    (f)
-    (mount/stop)))
-
-(deftest screen-css-smoke-test
-  (binding [context/*lang* :fi]
-    (is (string? (styles/screen-css)))))
diff --git a/test/clj/rems/api/test_applications.clj b/test/clj/rems/api/test_applications.clj
index fd37166bc..4e4338344 100644
--- a/test/clj/rems/api/test_applications.clj
+++ b/test/clj/rems/api/test_applications.clj
@@ -3,7 +3,6 @@
             [clojure.java.io :as io]
             [clojure.string :as str]
             [clojure.test :refer :all]
-            [rems.api.services.attachment :as attachment]
             [rems.api.services.catalogue :as catalogue]
             [rems.api.testing :refer :all]
             [rems.db.applications]
@@ -241,7 +240,6 @@
                  "application.command/change-resources"
                  "application.command/close"
                  "application.command/assign-external-id"
-                 "application.command/change-applicant"
                  "see-everything"}
                (set (get application :application/permissions))))))
 
@@ -265,7 +263,6 @@
                      "application.command/change-resources"
                      "application.command/close"
                      "application.command/assign-external-id"
-                     "application.command/change-applicant"
                      "see-everything"}
                    (set (get application :application/permissions))))))
         (testing "disabled command fails"
@@ -749,61 +746,6 @@
                   "application.command/approve"]
                  (:application/permissions (get-application-for-user app-id decider)))))))))
 
-(deftest test-change-applicant
-  (let [applicant "alice"
-        handler "developer"
-        app-id (test-helpers/create-application! {:actor applicant})]
-    (testing "submit application and add two members"
-      (is (= {:success true}
-             (send-command applicant {:type :application.command/submit
-                                      :application-id app-id})))
-      (is (= {:success true}
-             (send-command handler {:type :application.command/add-member
-                                    :application-id app-id
-                                    :member {:userid "carl"}})))
-      (is (= {:success true}
-             (send-command handler {:type :application.command/add-member
-                                    :application-id app-id
-                                    :member {:userid "malice"}}))))
-    (testing "promote carl to applicant"
-      (is (= {:success true}
-             (send-command handler {:type :application.command/change-applicant
-                                    :application-id app-id
-                                    :member {:userid "carl"}})))
-      (let [application (get-application-for-user app-id applicant)]
-        (is (= "carl" (get-in application [:application/applicant :userid])))
-        (is (= #{"alice" "malice"} (->> application :application/members (map :userid) set)))
-        (testing "alice has limited rights"
-          (is (= #{"application.command/copy-as-new"
-                   "application.command/accept-licenses"}
-                 (-> application :application/permissions set)))))
-      (testing "carl has full rights"
-        (is (= #{"application.command/copy-as-new"
-                 "application.command/remove-member"
-                 "application.command/accept-licenses"
-                 "application.command/uninvite-member"}
-               (-> (get-application-for-user app-id "carl")
-                   :application/permissions
-                   set)))))
-    (testing "applicant can't promote member to applicant"
-      (is (= {:success false :errors [{:type "forbidden"}]}
-             (send-command "carl" {:type :application.command/change-applicant
-                                   :application-id app-id
-                                   :member {:userid "malice"}}))))
-    (testing "can't promote non-member to applicant"
-      (is (= {:success false :errors [{:type "user-not-member" :user {:userid "elsa"}}]}
-             (send-command handler {:type :application.command/change-applicant
-                                    :application-id app-id
-                                    :member {:userid "elsa"}}))))
-    (testing "can't change applicant of returned application"
-      (is (= {:success true}
-             (send-command handler {:type :application.command/return
-                                    :application-id app-id})))
-      (is (= {:success false :errors [{:type "forbidden"}]}
-             (send-command handler {:type :application.command/change-applicant
-                                    :application-id app-id
-                                    :member {:userid "malice"}}))))))
-
 (deftest test-application-validation
   (let [user-id "alice"
         form-id (test-helpers/create-form! {:form/fields [{:field/id "req1"
@@ -1110,7 +1052,6 @@
                "application.command/request-review"
                "application.command/return"
                "application.command/uninvite-member"
-               "application.command/change-applicant"
                "see-everything"}
              (set (:application/permissions (get-application-for-user app-id handler))))))
     (testing "request decision"
@@ -1623,8 +1564,8 @@
                              read-ok-body
                              :id))
         file #(assoc filecontent :filename %)
-        fetch-zip (fn [user-id & [params]]
-                    (with-open [zip (-> (api-response :get (str "/api/applications/" app-id "/attachments" params) nil
+        fetch-zip (fn [user-id]
+                    (with-open [zip (-> (api-response :get (str "/api/applications/" app-id "/attachments") nil
                                                       api-key user-id)
                                         :body
                                         ZipInputStream.)]
@@ -1653,9 +1594,7 @@
       (is (= {"blue.txt" (slurp testfile)
               "red.txt" (slurp testfile)
               "green.txt" (slurp testfile)}
-             (fetch-zip applicant-id)
-             (fetch-zip applicant-id "?all=true")
-             (fetch-zip applicant-id "?all=false"))))
+             (fetch-zip applicant-id))))
     (testing "submit"
       (is (= {:success true}
              (send-command applicant-id {:type :application.command/submit
@@ -1676,17 +1615,9 @@
                 "green.txt" (slurp testfile)
                 "blue (1).txt" (slurp testfile)
                 "yellow.txt" (slurp testfile)}
-               (fetch-zip applicant-id "?all=true")
                (fetch-zip applicant-id)
                (fetch-zip handler-id)
                (fetch-zip reporter-id))))
-      (testing "fetch zip with all=false as applicant, handler and reporter"
-        (is (= {"blue.txt" (slurp testfile)
-                "red.txt" (slurp testfile)
-                "green.txt" (slurp testfile)}
-               (fetch-zip applicant-id "?all=false")
-               (fetch-zip handler-id "?all=false")
-               (fetch-zip reporter-id "?all=false"))))
       (testing "fetch zip as third party"
         (is (response-is-forbidden? (api-response :get (str "/api/applications/" app-id "/attachments") nil
                                                   api-key "malice"))))
@@ -1963,15 +1894,9 @@
                                                                   :sv "Notifications SV"}
                                             :form/fields [{:field/type :text
                                                            :field/id "field-1"
-                                                           :field/title {:en "text field EN"
-                                                                         :fi "text field FI"
-                                                                         :sv "text field SV"}
-                                                           :field/optional false}
-                                                          {:field/type :attachment
-                                                           :field/id "att"
-                                                           :field/title {:en "attachment EN"
-                                                                         :fi "attachment FI"
-                                                                         :sv "attachment SV"}
+                                                           :field/title {:en "text field"
+                                                                         :fi "text field"
+                                                                         :sv "text field"}
                                                            :field/optional false}]})
         workflow-id (test-helpers/create-workflow! {:title "wf"
                                                     :handlers [handler]
@@ -1980,17 +1905,8 @@
         res-id (test-helpers/create-resource! {:resource-ext-id ext-id})
         cat-id (test-helpers/create-catalogue-item! {:form-id form-id
                                                      :resource-id res-id
-                                                     :workflow-id workflow-id
-                                                     :start (time/date-time 2009)})
-        app-id (test-helpers/create-application! {:time (time/date-time 2010)
-                                                  :actor applicant
-                                                  :catalogue-item-ids [cat-id]})
-        att-id (:id (attachment/add-application-attachment applicant app-id filecontent))]
-    (test-helpers/fill-form! {:time (time/date-time 2010)
-                              :application-id app-id
-                              :actor applicant
-                              :field-value "raw test"
-                              :attachment att-id})
+                                                     :workflow-id workflow-id})
+        app-id (test-helpers/create-draft! applicant [cat-id] "raw test" (time/date-time 2010))]
     (testing "applicant can't get raw application"
       (is (response-is-forbidden? (api-response :get (str "/api/applications/" app-id "/raw") nil
                                                 api-key applicant))))
@@ -1998,9 +1914,7 @@
       (is (= {:application/description ""
               :application/invited-members []
               :application/last-activity "2010-01-01T00:00:00.000Z"
-              :application/attachments [{:attachment/type "text/plain"
-                                         :attachment/filename "test.txt"
-                                         :attachment/id att-id}]
+              :application/attachments []
               :application/licenses []
               :application/created "2010-01-01T00:00:00.000Z"
               :application/state "application.state/draft"
@@ -2031,7 +1945,7 @@
               :application/todo nil
               :application/applicant {:email "alice@example.com" :userid "alice" :name "Alice Applicant" :nickname "In Wonderland" :organizations [{:organization/id "default"}] :researcher-status-by "so"}
               :application/members []
-              :application/resources [{:catalogue-item/start "2009-01-01T00:00:00.000Z"
+              :application/resources [{:catalogue-item/start "REDACTED"
                                        :catalogue-item/end nil
                                        :catalogue-item/expired false
                                        :catalogue-item/enabled true
@@ -2041,24 +1955,15 @@
                                        :resource/ext-id ext-id
                                        :catalogue-item/archived false
                                        :catalogue-item/id cat-id}]
-              :application/accepted-licenses {}
+              :application/accepted-licenses {:alice []}
               :application/forms [{:form/fields [{:field/value "raw test"
                                                   :field/type "text"
-                                                  :field/title {:en "text field EN"
-                                                                :fi "text field FI"
-                                                                :sv "text field SV"}
+                                                  :field/title {:en "text field"
+                                                                :fi "text field"
+                                                                :sv "text field"}
                                                   :field/id "field-1"
                                                   :field/optional false
                                                   :field/visible true
-                                                  :field/private false}
-                                                 {:field/value (str att-id)
-                                                  :field/type "attachment"
-                                                  :field/id "att"
-                                                  :field/title {:en "attachment EN"
-                                                                :fi "attachment FI"
-                                                                :sv "attachment SV"}
-                                                  :field/optional false
-                                                  :field/visible true
                                                   :field/private false}]
                                    :form/title "notifications" ; deprecated
                                    :form/internal-name "notifications"
@@ -2084,9 +1989,17 @@
                                     :event/actor "alice"
                                     :application/id app-id
                                     :event/actor-attributes {:userid "alice" :name "Alice Applicant" :nickname "In Wonderland" :email "alice@example.com" :organizations [{:organization/id "default"}] :researcher-status-by "so"}
-                                    :application/field-values [{:form form-id :field "field-1" :value "raw test"}
-                                                               {:form form-id :field "att" :value (str att-id)}]}]}
+                                    :application/field-values [{:form form-id :field "field-1" :value "raw test"}]}
+                                   {:event/id 100
+                                    :event/type "application.event/licenses-accepted"
+                                    :event/time "2010-01-01T00:00:00.000Z"
+                                    :event/actor "alice"
+                                    :application/id app-id
+                                    :event/actor-attributes {:userid "alice" :name "Alice Applicant" :nickname "In Wonderland" :email "alice@example.com" :organizations [{:organization/id "default"}] :researcher-status-by "so"}
+                                    :application/accepted-licenses []}]}
              (-> (api-call :get (str "/api/applications/" app-id "/raw") nil
                            api-key reporter)
+                 ;; start is set by the db not easy to mock
+                 (assoc-in [:application/resources 0 :catalogue-item/start] "REDACTED")
                  ;; event ids are unpredictable
                  (update :application/events (partial map #(update % :event/id (constantly 100))))))))))
diff --git a/test/clj/rems/application/test_commands.clj b/test/clj/rems/application/test_commands.clj
index 26b9dfb41..4a9cf1f4e 100644
--- a/test/clj/rems/application/test_commands.clj
+++ b/test/clj/rems/application/test_commands.clj
@@ -1594,59 +1594,6 @@
                             :comment ""}
                            injections))))))
 
-(deftest test-change-applicant
-  (let [application (apply-events nil
-                                  [dummy-created-event
-                                   {:event/type :application.event/submitted
-                                    :event/time test-time
-                                    :event/actor applicant-user-id
-                                    :application/id app-id}
-                                   {:event/type :application.event/member-added
-                                    :event/time test-time
-                                    :event/actor handler-user-id
-                                    :application/id app-id
-                                    :application/member {:userid "member1"}}])]
-    (testing "handler can promote existing member"
-      (is (= {:event/type :application.event/applicant-changed
-              :event/time test-time
-              :event/actor handler-user-id
-              :application/id app-id
-              :application/applicant {:userid "member1"}
-              :application/comment ""}
-             (ok-command application
-                         {:type :application.command/change-applicant
-                          :actor handler-user-id
-                          :member {:userid "member1"}
-                          :comment ""}))))
-    (testing "applicant can't promote"
-      (is (= {:errors [{:type :forbidden}]}
-             (fail-command application
-                           {:type :application.command/change-applicant
-                            :actor applicant-user-id
-                            :member {:userid "member1"}
-                            :comment ""}))))
-    (testing "member can't promote themself"
-      (is (= {:errors [{:type :forbidden}]}
-             (fail-command application
-                           {:type :application.command/change-applicant
-                            :actor "member1"
-                            :member {:userid "member1"}
-                            :comment ""}))))
-    (testing "handler can't promote non-member"
-      (is (= {:errors [{:type :user-not-member :user {:userid "unknown"}}]}
-             (fail-command application
-                           {:type :application.command/change-applicant
-                            :actor handler-user-id
-                            :member {:userid "unknown"}
-                            :comment ""}))))
-    (testing "handler can't promote current applicant"
-      (is (= {:errors [{:type :user-not-member :user {:userid "applicant"}}]}
-             (fail-command application
-                           {:type :application.command/change-applicant
-                            :actor handler-user-id
-                            :member {:userid applicant-user-id}
-                            :comment ""}))))))
-
 (deftest test-review
   (let [reviewer "reviewer"
         reviewer2 "reviewer2"
diff --git a/test/clj/rems/application/test_master_workflow.clj b/test/clj/rems/application/test_master_workflow.clj
index 7dddbd419..dce1e8612 100644
--- a/test/clj/rems/application/test_master_workflow.clj
+++ b/test/clj/rems/application/test_master_workflow.clj
@@ -50,30 +50,4 @@
       (is (not (contains? (permissions/user-permissions closed "joe")
                           :application.command/accept-invitation)))
       (is (not (contains? (permissions/user-permissions closed "applicant")
-                          :application.command/accept-invitation)))))
-
-  (testing "applicant change"
-    (let [original (reduce application-permissions-view nil [{:event/type :application.event/created
-                                                              :event/actor "applicant"}
-                                                             {:event/type :application.event/submitted
-                                                              :event/actor "applicant"}
-                                                             {:event/type :application.event/member-added
-                                                              :event/actor "handler"
-                                                              :application/member {:userid "new"}}])
-          changed (reduce application-permissions-view original [{:event/type :application.event/applicant-changed
-                                                                  :event/actor "handler"
-                                                                  :application/applicant {:userid "new"}}])]
-      (is (= #{:application.command/copy-as-new
-               :application.command/remove-member
-               :application.command/accept-licenses
-               :application.command/uninvite-member}
-             (permissions/user-permissions original "applicant")))
-      (is (= #{:application.command/copy-as-new :application.command/accept-licenses}
-             (permissions/user-permissions original "new")))
-      (is (= #{:application.command/copy-as-new :application.command/accept-licenses}
-             (permissions/user-permissions changed "applicant")))
-      (is (= #{:application.command/copy-as-new
-               :application.command/remove-member
-               :application.command/accept-licenses
-               :application.command/uninvite-member}
-             (permissions/user-permissions changed "new"))))))
+                          :application.command/accept-invitation))))))
diff --git a/test/clj/rems/application/test_model.clj b/test/clj/rems/application/test_model.clj
index 2c6d1b759..03c0cc4b5 100644
--- a/test/clj/rems/application/test_model.clj
+++ b/test/clj/rems/application/test_model.clj
@@ -1044,61 +1044,6 @@
                                            :application/past-members #{{:userid "member"}}})]
           (is (= expected-application (recreate expected-application))))))))
 
-(deftest test-applicant-change
-  (let [application (-> submitted-application
-                        (update :application/events into [{:event/type :application.event/member-added
-                                                           :event/time (DateTime. 4000)
-                                                           :event/actor "handler"
-                                                           :application/id 1
-                                                           :application/member {:userid "member1"}}
-                                                          {:event/type :application.event/member-added
-                                                           :event/time (DateTime. 4000)
-                                                           :event/actor "handler"
-                                                           :application/id 1
-                                                           :application/member {:userid "member2"}}])
-                        recreate)]
-    (testing "> promote member to applicant"
-      (let [new-event {:event/type :application.event/applicant-changed
-                       :event/time (DateTime. 5000)
-                       :event/actor "handler"
-                       :application/id 1
-                       :application/applicant {:userid "member1"}}
-            events (conj (:application/events application) new-event)
-            expected-application (merge application
-                                        {:application/last-activity (DateTime. 5000)
-                                         :application/events events
-                                         :application/applicant {:userid "member1"}
-                                         :application/members #{{:userid "member2"} {:userid "applicant"}}})]
-        (is (= expected-application (recreate expected-application)))
-        (testing "> promote original applicant back"
-          (let [new-event {:event/type :application.event/applicant-changed
-                           :event/time (DateTime. 6000)
-                           :event/actor "handler"
-                           :application/id 1
-                           :application/applicant {:userid "applicant"}}
-                events (conj events new-event)
-                expected-application (merge expected-application
-                                            {:application/last-activity (DateTime. 6000)
-                                             :application/events events
-                                             :application/applicant {:userid "applicant"}
-                                             :application/members #{{:userid "member1"} {:userid "member2"}}})]
-            (is (= expected-application (recreate expected-application)))))))
-    ;; This isn't currently a planned use case for the event, just
-    ;; documenting this possibility.
-    (testing "> promote other user to applicant"
-      (let [new-event {:event/type :application.event/applicant-changed
-                       :event/time (DateTime. 5000)
-                       :event/actor "handler"
-                       :application/id 1
-                       :application/applicant {:userid "usurper"}}
-            events (conj (:application/events application) new-event)
-            expected-application (merge application
-                                        {:application/last-activity (DateTime. 5000)
-                                         :application/events events
-                                         :application/applicant {:userid "usurper"}
-                                         :application/members #{{:userid "member1"} {:userid "member2"} {:userid "applicant"}}})]
-        (is (= expected-application (recreate expected-application)))))))
-
 (deftest test-application-view-actor-invitations
   (testing "> reviewer invited"
     (let [token "abcd1234"
diff --git a/test/clj/rems/browser_test_util.clj b/test/clj/rems/browser_test_util.clj
index 748217141..f8cad48c7 100644
--- a/test/clj/rems/browser_test_util.clj
+++ b/test/clj/rems/browser_test_util.clj
@@ -17,8 +17,7 @@
             [rems.db.test-data-users :as test-users]
             [rems.db.test-data :as test-data]
             [rems.json :as json]
-            [rems.standalone]
-            [slingshot.slingshot :refer [try+]])
+            [rems.standalone])
   (:import (java.net SocketException)))
 
 ;;; test setup
@@ -293,21 +292,8 @@
 ;; TODO add more of etaoin here
 
 
-;;; etaoin extensions
-
-;; exceptions make for ugly test failures: here are some wrappers that
-;; are better adapted for (is ...) assertions
 
-(defn no-timeout? [f]
-  (try+
-   (f)
-   true
-   (catch [:type :etaoin/timeout] e
-     (log/error e)
-     false)))
-
-(defn eventually-visible? [& args]
-  (no-timeout? #(apply wait-visible args)))
+;;; etaoin extensions
 
 ;; TODO our input fields process every character through re-frame.
 ;; Etaoin's fill-human almost works, but very rarely loses characters,
diff --git a/test/clj/rems/email/test_template.clj b/test/clj/rems/email/test_template.clj
index e39bb6780..69da51cf9 100644
--- a/test/clj/rems/email/test_template.clj
+++ b/test/clj/rems/email/test_template.clj
@@ -78,9 +78,6 @@
                  :email "assistant@example.com"}
     "remarker" {:userid "remarker"
                 :name "Random Remarker"}
-    "bob" {:userid "bob"
-           :name "Bob Boss"
-           :email "bob@corp.net"}
     {:userid userid}))
 
 (defn email-recipient [email]
@@ -398,26 +395,6 @@
                               :application/decider {:email "actor@example.com" :name "Adam Actor"}
                               :invitation/token "abc123"}))))
 
-(deftest test-change-applicant
-  (let [change {:application/id 7
-                :event/type :application.event/applicant-changed
-                :event/actor "assistant"
-                :application/applicant {:userid "bob"}}
-        mails (emails base-events change)]
-    (is (= #{"applicant" "handler" "bob"} (email-recipients mails)))
-    (is (= {:to-user "applicant"
-            :subject "Applicant for application 2001/3, \"Application title\" changed"
-            :body "Dear Alice Applicant,\n\nThe applicant for application 2001/3, \"Application title\" has been changed to Bob Boss by Amber Assistant.\n\nYou can view the application at http://example.com/application/7."}
-           (email-to "applicant" mails)))
-    (is (= {:to-user "bob"
-            :subject "Applicant for application 2001/3, \"Application title\" changed"
-            :body "Dear Bob Boss,\n\nThe applicant for application 2001/3, \"Application title\" has been changed to Bob Boss by Amber Assistant.\n\nYou can view the application at http://example.com/application/7."}
-           (email-to "bob" mails)))
-    (is (= {:to-user "handler"
-            :subject "(2001/3, \"Application title\") Applicant changed"
-            :body "Dear Hannah Handler,\n\nThe applicant for application 2001/3, \"Application title\" has been changed to Bob Boss by Amber Assistant.\n\nYou can view the application at http://example.com/application/7."}
-           (email-to "handler" mails)))))
-
 (deftest test-finnish-emails
   ;; only one test case so far, more of a smoke test
   (with-redefs [rems.locales/translations (assoc-in rems.locales/translations [:fi :t :email :regards] "")]
diff --git a/test/clj/rems/test_browser.clj b/test/clj/rems/test_browser.clj
index 60e4d506a..56a6c72b0 100644
--- a/test/clj/rems/test_browser.clj
+++ b/test/clj/rems/test_browser.clj
@@ -48,10 +48,6 @@
   (btu/scroll-and-click {:css ".login-btn"})
   (btu/screenshot "login-page.png")
   (btu/scroll-and-click [{:css ".users"} {:tag :a :fn/text username}])
-  ;; NB. it's better to use wait-visible instead of
-  ;; eventually-visible? in utils like this because we want the line
-  ;; in the original test in the error, not a line in this util with
-  ;; no info of where it was called from.
   (btu/wait-visible :logout)
   (btu/screenshot "logged-in.png"))
 
@@ -326,9 +322,9 @@
         (btu/check-box "Option3")
         ;; fill in two rows for the table
         (btu/scroll-and-click (keyword (str table-field-id "-add-row")))
-        (is (btu/eventually-visible? (keyword (str table-field-id "-row0-col1"))))
+        (btu/wait-visible (keyword (str table-field-id "-row0-col1")))
         (btu/scroll-and-click (keyword (str table-field-id "-add-row")))
-        (is (btu/eventually-visible? (keyword (str table-field-id "-row1-col1"))))
+        (btu/wait-visible (keyword (str table-field-id "-row1-col1")))
         (btu/fill-human (keyword (str table-field-id "-row0-col1")) "a")
         (btu/fill-human (keyword (str table-field-id "-row0-col2")) "b")
         (btu/fill-human (keyword (str table-field-id "-row1-col1")) "c")
@@ -342,12 +338,12 @@
 
         (testing "save draft succesfully"
           (btu/scroll-and-click :save)
-          (is (btu/eventually-visible? :status-success)))
+          (btu/wait-visible :status-success))
 
         (testing "add invalid value for field, try to save"
           (fill-form-field "Email field" "user")
           (btu/scroll-and-click :save)
-          (is (btu/eventually-visible? :status-failed))
+          (btu/wait-visible :status-failed)
           (is (= ["Invalid email address."]
                  (get-validation-summary)))
           (is (= "Invalid email address."
@@ -359,7 +355,7 @@
 
         (testing "try to submit without accepting licenses or filling in a mandatory field"
           (btu/scroll-and-click :submit)
-          (is (btu/eventually-visible? {:id "status-failed" :fn/has-text "Send application"}))
+          (btu/wait-visible {:id "status-failed" :fn/has-text "Send application"})
           (is (= ["Terms of use not accepted."
                   "Field \"Text field\" is required."]
                  (get-validation-summary)))
@@ -439,7 +435,7 @@
               (btu/scroll-and-click [{:css "table.my-applications"}
                                      {:tag :tr :data-row (btu/context-get :application-id)}
                                      {:css ".btn-primary"}])
-              (is (btu/eventually-visible? {:tag :h1 :fn/has-text "Application"}))
+              (btu/wait-visible {:tag :h1 :fn/has-text "Application"})
               (btu/wait-page-loaded)
               (btu/gather-axe-results)
               (testing "check a field answer"
@@ -461,16 +457,16 @@
     (testing "invite member"
       (is (not (btu/visible? [:actions-invite-member {:fn/has-text "Invite member"}])))
       (btu/scroll-and-click :invite-member-action-button)
-      (is (btu/eventually-visible? [:actions-invite-member {:fn/has-text "Invite member"}]))
+      (btu/wait-visible [:actions-invite-member {:fn/has-text "Invite member"}])
       (btu/fill-human [:actions-invite-member :name-invite-member] "John Smith")
       (btu/fill-human [:actions-invite-member :email-invite-member] "john.smith@generic.name")
       (btu/scroll-and-click :invite-member)
       (btu/wait-invisible [:actions-invite-member {:fn/has-text "Invite member"}])
       (btu/scroll-and-click :invite0-info-collapse-more-link)
-      (is (btu/eventually-visible? :invite0-info-collapse))
+      (btu/wait-visible :invite0-info-collapse)
 
       (is (= {"Name" "John Smith"
-              "Email" "john.smith@generic.name"}
+              "Email (from identity provider)" "john.smith@generic.name"}
              (slurp-fields :invite0-info)))
       (is (string? (-> (btu/context-get :application-id)
                        applications/get-application-internal
@@ -488,13 +484,13 @@
       (is (btu/visible? {:css "div.event-description" :fn/text "Alice Applicant invited John Smith to the application."})))
 
     (testing "uninvite member"
-      (is (not (btu/visible? :actions-invite0-operations-remove)))
-      (btu/scroll-and-click :invite0-operations-remove-action-button)
-      (is (btu/eventually-visible? :actions-invite0-operations-remove))
-      (btu/fill-human :comment-invite0-operations-remove-comment "sorry but no")
-      (btu/scroll-and-click :invite0-operations-remove-submit)
-      (is (btu/eventually-visible? [{:css ".alert-success" :fn/has-text "Remove member: Success"}]))
-      (btu/wait-invisible :actions-invite0-operations-remove)
+      (is (not (btu/visible? :actions-invite0-remove-member-form)))
+      (btu/scroll-and-click :invite0-remove-member-form-action-button)
+      (btu/wait-visible :actions-invite0-remove-member-form)
+      (btu/fill-human :comment-invite0-remove-member-comment "sorry but no")
+      (btu/scroll-and-click :invite0-remove-member-submit)
+      (btu/wait-visible [{:css ".alert-success" :fn/has-text "Remove member: Success"}])
+      (btu/wait-invisible :actions-invite0-remove-member-form)
       (btu/wait-invisible :invite0-info)
 
       (is (empty? (-> (btu/context-get :application-id)
@@ -537,13 +533,13 @@
     (go-to-application (btu/context-get :application-id))
 
     (testing "remove second member jade"
-      (is (not (btu/visible? :actions-member1-operations-remove)))
-      (btu/scroll-and-click :member1-operations-remove-action-button)
-      (is (btu/eventually-visible? :actions-member1-operations-remove))
-      (btu/fill-human :comment-member1-operations-remove-comment "not in research group anymore")
-      (btu/scroll-and-click :member1-operations-remove-submit)
-      (is (btu/eventually-visible? [{:css ".alert-success" :fn/has-text "Remove member: Success"}]))
-      (btu/wait-invisible :actions-member1-operations-remove)
+      (is (not (btu/visible? :actions-member1-remove-member-form)))
+      (btu/scroll-and-click :member1-remove-member-form-action-button)
+      (btu/wait-visible :actions-member1-remove-member-form)
+      (btu/fill-human :comment-member1-remove-member-comment "not in research group anymore")
+      (btu/scroll-and-click :member1-remove-member-submit)
+      (btu/wait-visible [{:css ".alert-success" :fn/has-text "Remove member: Success"}])
+      (btu/wait-invisible :actions-member1-remove-member-form)
       (btu/wait-invisible :member2-info) ; last element is removed from DOM, remaining updated
 
       (is (= #{{:userid "ionna" :name "Ionna Insprucker" :email "ionna@ins.mail"}
@@ -569,15 +565,15 @@
   (btu/with-postmortem
     (login-as "developer")
     (testing "handler should see todos on logging in"
-      (is (btu/eventually-visible? :todo-applications)))
+      (btu/wait-visible :todo-applications))
     (testing "handler should see description of application"
-      (is (btu/eventually-visible? {:class :application-description :fn/text "test-handling"})))
+      (btu/wait-visible {:class :application-description :fn/text "test-handling"}))
     (let [app-button {:tag :a :href (str "/application/" (btu/context-get :application-id))}]
       (testing "handler should see view button for application"
-        (is (btu/eventually-visible? app-button)))
+        (btu/wait-visible app-button))
       (btu/scroll-and-click app-button))
     (testing "handler should see application after clicking on View"
-      (is (btu/eventually-visible? {:tag :h1 :fn/has-text "test-handling"})))
+      (btu/wait-visible {:tag :h1 :fn/has-text "test-handling"}))
     (testing "handler should see the applicant info"
       (btu/scroll-and-click :applicant-info-collapse-more-link)
       (is (= {"Name" "Alice Applicant"
@@ -591,10 +587,10 @@
     (testing "open the approve form"
       (btu/scroll-and-click :approve-reject-action-button))
     (testing "add a comment and two attachments"
-      (is (btu/eventually-visible? :comment-approve-reject))
+      (btu/wait-visible :comment-approve-reject)
       (btu/fill-human :comment-approve-reject "this is a comment")
       (btu/upload-file :upload-approve-reject-input "test-data/test.txt")
-      (is (btu/eventually-visible? [{:css "a.attachment-link"}]))
+      (btu/wait-visible [{:css "a.attachment-link"}])
       (btu/upload-file :upload-approve-reject-input "test-data/test-fi.txt")
       (btu/wait-predicate #(= ["test.txt" "test-fi.txt"]
                               (get-attachments))))
@@ -640,17 +636,17 @@
     (testing "handler invites reviewer"
       (login-as "developer")
       (go-to-application (btu/context-get :application-id))
-      (is (btu/eventually-visible? {:tag :h1 :fn/has-text "test-invite-decider"}))
+      (btu/wait-visible {:tag :h1 :fn/has-text "test-invite-decider"})
 
       (btu/scroll-and-click :request-decision-dropdown)
-      (is (btu/eventually-visible? :invite-decider-action-button))
+      (btu/wait-visible :invite-decider-action-button)
       (btu/scroll-and-click :invite-decider-action-button)
 
-      (is (btu/eventually-visible? :name-invite-decider))
+      (btu/wait-visible :name-invite-decider)
       (btu/fill-human :name-invite-decider "anybody will do")
       (btu/fill-human :email-invite-decider "user@example.com")
       (btu/scroll-and-click :invite-decider)
-      (is (btu/eventually-visible? {:css ".alert-success"}))
+      (btu/wait-visible {:css ".alert-success"})
       (logout))
     (testing "get invite token"
       (let [[token invitation] (-> (btu/context-get :application-id)
@@ -664,12 +660,12 @@
         (btu/context-assoc! :token token)))
     (testing "accept invitation"
       (btu/go (str (btu/get-server-url) "application/accept-invitation/" (btu/context-get :token)))
-      (is (btu/eventually-visible? {:css ".login-btn"}))
+      (btu/wait-visible {:css ".login-btn"})
       (btu/scroll-and-click {:css ".login-btn"})
-      (is (btu/eventually-visible? [{:css ".users"} {:tag :a :fn/text "new-reviewer"}]))
+      (btu/wait-visible [{:css ".users"} {:tag :a :fn/text "new-reviewer"}])
       (btu/scroll-and-click [{:css ".users"} {:tag :a :fn/text "new-reviewer"}])
       (btu/wait-page-loaded)
-      (is (btu/eventually-visible? {:tag :h1 :fn/has-text "test-invite-decider"})))
+      (btu/wait-visible {:tag :h1 :fn/has-text "test-invite-decider"}))
     (testing "check decider-joined event"
       (is (= {:event/type :application.event/decider-joined
               :event/actor "new-reviewer"}
@@ -680,11 +676,11 @@
                  (select-keys [:event/actor :event/type])))))
     (testing "submit decision"
       (btu/scroll-and-click :decide-action-button)
-      (is (btu/eventually-visible? :comment-decide))
+      (btu/wait-visible :comment-decide)
       (btu/fill-human :comment-decide "ok")
       (btu/scroll-and-click :decide-approve)
       (btu/wait-page-loaded)
-      (is (btu/eventually-visible? {:css ".alert-success"})))
+      (btu/wait-visible {:css ".alert-success"}))
     (testing "check decision event"
       (is (= {:application/decision :approved
               :application/comment "ok"
@@ -711,10 +707,10 @@
   (btu/with-postmortem
     (login-as "developer")
     (btu/go (str (btu/get-server-url) "application/" (btu/context-get :application-id)))
-    (is (btu/eventually-visible? {:tag :h1 :fn/has-text "test-approve-with-end-date"}))
+    (btu/wait-visible {:tag :h1 :fn/has-text "test-approve-with-end-date"})
     (testing "approve"
       (btu/scroll-and-click :approve-reject-action-button)
-      (is (btu/eventually-visible? :comment-approve-reject))
+      (btu/wait-visible :comment-approve-reject)
       (btu/fill-human :comment-approve-reject "this is a comment")
       (set-date :approve-end "2100-05-06")
       (btu/scroll-and-click :approve)
@@ -735,7 +731,7 @@
 (deftest test-guide-page
   (btu/with-postmortem
     (btu/go (str (btu/get-server-url) "guide"))
-    (is (btu/eventually-visible? {:tag :h1 :fn/text "Component Guide"}))
+    (btu/wait-visible {:tag :h1 :fn/text "Component Guide"})
     ;; if there is a js exception, nothing renders, so let's check
     ;; that we have lots of examples in the dom:
     (is (< 60 (count (btu/query-all {:class :example}))))))
@@ -744,20 +740,20 @@
   (btu/with-postmortem
     (testing "default language is English"
       (btu/go (btu/get-server-url))
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Welcome to REMS"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Welcome to REMS"})
       (login-as "alice")
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Catalogue"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Catalogue"})
       (btu/wait-page-loaded))
 
     (testing "changing language while logged out"
       (logout)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Welcome to REMS"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Welcome to REMS"})
       (change-language :fi)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Tervetuloa REMSiin"})))
+      (btu/wait-visible {:tag :h1 :fn/text "Tervetuloa REMSiin"}))
 
     (testing "changed language must persist after login"
       (login-as "alice")
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Aineistoluettelo"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Aineistoluettelo"})
       (btu/wait-page-loaded))
 
     (testing "wait for language change to show in the db"
@@ -766,14 +762,14 @@
     (testing "changed language must have been saved for user"
       (logout)
       (change-language :en)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Welcome to REMS"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Welcome to REMS"})
       (btu/delete-cookies)
       (login-as "alice")
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Aineistoluettelo"})))
+      (btu/wait-visible {:tag :h1 :fn/text "Aineistoluettelo"}))
 
     (testing "changing language while logged in"
       (change-language :en)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Catalogue"})))
+      (btu/wait-visible {:tag :h1 :fn/text "Catalogue"}))
 
     (user-settings/delete-user-settings! "alice") ; clear language settings
     (is true))) ; avoid no assertions warning
@@ -783,7 +779,7 @@
     (btu/with-postmortem
       (go-to-admin "Licenses")
       (btu/scroll-and-click :create-license)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Create license"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Create license"})
       (btu/wait-page-loaded)
       (select-option "Organization" "nbn")
       (btu/scroll-and-click :licensetype-link)
@@ -795,7 +791,7 @@
       (fill-form-field "License link" "https://www.csc.fi/home" {:index 3})
       (btu/screenshot "about-to-create-license.png")
       (btu/scroll-and-click :save)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "License"}))
+      (btu/wait-visible {:tag :h1 :fn/text "License"})
       (btu/wait-page-loaded)
       (btu/screenshot "created-license.png")
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
@@ -810,7 +806,7 @@
               "Active" true}
              (slurp-fields :license)))
       (go-to-admin "Licenses")
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Licenses"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Licenses"})
       (is (some #{{"organization" "NBN"
                    "title" (str (btu/context-get :license-name) " EN")
                    "type" "link"
@@ -820,7 +816,7 @@
       (click-row-action [:licenses]
                         {:fn/text (str (btu/context-get :license-name) " EN")}
                         (select-button-by-label "View"))
-      (is (btu/eventually-visible? :license))
+      (btu/wait-visible :license)
       (is (= {"Organization" "NBN"
               "Title (EN)" (str (btu/context-get :license-name) " EN")
               "Title (FI)" (str (btu/context-get :license-name) " FI")
@@ -837,14 +833,14 @@
     (btu/with-postmortem
       (go-to-admin "Resources")
       (btu/scroll-and-click :create-resource)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Create resource"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Create resource"})
       (btu/wait-page-loaded)
       (select-option "Organization" "nbn")
       (fill-form-field "Resource identifier" (btu/context-get :resid))
       (select-option "License" (str (btu/context-get :license-name) " EN"))
       (btu/screenshot "about-to-create-resource.png")
       (btu/scroll-and-click :save)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Resource"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Resource"})
       (btu/wait-page-loaded)
       (btu/screenshot "created-resource.png")
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
@@ -864,7 +860,7 @@
     (btu/with-postmortem
       (go-to-admin "Forms")
       (btu/scroll-and-click :create-form)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Create form"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Create form"})
       (btu/wait-page-loaded)
       (select-option "Organization" "nbn")
       (fill-form-field "Name" (btu/context-get :form-name))
@@ -874,7 +870,7 @@
       ;; TODO: create fields
       (btu/screenshot "about-to-create-form.png")
       (btu/scroll-and-click :save)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Form"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Form"})
       (btu/wait-page-loaded)
       (btu/screenshot "created-form.png")
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
@@ -894,7 +890,7 @@
     (btu/with-postmortem
       (go-to-admin "Workflows")
       (btu/scroll-and-click :create-workflow)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Create workflow"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Create workflow"})
       (btu/wait-page-loaded)
       (select-option "Organization" "nbn")
       (fill-form-field "Title" (btu/context-get :workflow-name))
@@ -903,7 +899,7 @@
       ;; No form
       (btu/screenshot "about-to-create-workflow.png")
       (btu/scroll-and-click :save)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Workflow"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Workflow"})
       (btu/wait-page-loaded)
       (btu/screenshot "created-workflow.png")
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
@@ -923,7 +919,7 @@
     (btu/with-postmortem
       (go-to-admin "Catalogue items")
       (btu/scroll-and-click :create-catalogue-item)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Create catalogue item"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Create catalogue item"})
       (btu/wait-page-loaded)
       (select-option "Organization" "nbn")
       (fill-form-field "Title" (btu/context-get :catalogue-item-name) {:index 1})
@@ -935,7 +931,7 @@
         (select-option "Form" form-name))
       (btu/screenshot "about-to-create-catalogue-item.png")
       (btu/scroll-and-click :save)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Catalogue item"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Catalogue item"})
       (btu/wait-page-loaded)
       (btu/screenshot "created-catalogue-item.png")
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
@@ -1037,7 +1033,7 @@
     (login-as "owner")
     (btu/go (str (btu/get-server-url) "administration/catalogue-items/edit/" (btu/context-get :catalogue-item)))
     (btu/wait-page-loaded)
-    (is (btu/eventually-visible? {:id :title-en :value "test-edit-catalogue-item EN"}))
+    (btu/wait-visible {:id :title-en :value "test-edit-catalogue-item EN"})
     (btu/screenshot "test-edit-catalogue-item-1.png")
     (is (= {"Organization" (str (btu/context-get :organization-name) " en")
             "Title (EN)" "test-edit-catalogue-item EN"
@@ -1053,7 +1049,7 @@
     (btu/fill-human :infourl-en "http://google.com")
     (btu/screenshot "test-edit-catalogue-item-2.png")
     (btu/scroll-and-click :save)
-    (is (btu/eventually-visible? {:tag :h1 :fn/text "Catalogue item"}))
+    (btu/wait-visible {:tag :h1 :fn/text "Catalogue item"})
     (btu/wait-page-loaded)
     (is (= {"Organization" (str (btu/context-get :organization-name) " en")
             "Title (EN)" "test-edit-catalogue-item EN"
@@ -1077,7 +1073,7 @@
       (testing "editing"
         (btu/go (str (btu/get-server-url) "administration/catalogue-items/edit/" (btu/context-get :catalogue-item)))
         (btu/wait-page-loaded)
-        (is (btu/eventually-visible? {:id :title-en :value "test-edit-catalogue-item EN"}))
+        (btu/wait-visible {:id :title-en :value "test-edit-catalogue-item EN"})
         (is (= {"Organization" "Select..." ; unable to select a disabled org again
                 "Title (EN)" "test-edit-catalogue-item EN"
                 "Title (FI)" "test-edit-catalogue-item FI"
@@ -1092,7 +1088,7 @@
       (testing "viewing"
         (btu/scroll-and-click :cancel)
         (btu/wait-page-loaded)
-        (is (btu/eventually-visible? {:id :title :fn/has-text "test-edit-catalogue-item EN"}))
+        (btu/wait-visible {:id :title :fn/has-text "test-edit-catalogue-item EN"})
         (is (= {"Organization" (str (btu/context-get :organization-name) " en")
                 "Title (EN)" "test-edit-catalogue-item EN"
                 "Title (FI)" "test-edit-catalogue-item FI"
@@ -1114,7 +1110,7 @@
 
     (testing "create form"
       (btu/scroll-and-click :create-form)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Create form"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Create form"})
       (select-option "Organization" "nbn")
       (fill-form-field "Name" "Form editor test")
       (fill-form-field "EN" "Form Editor Test (EN)")
@@ -1122,17 +1118,17 @@
       (fill-form-field "SV" "Form Editor Test (SV)")
       (btu/scroll-and-click {:class :add-form-field})
       ;; using ids to fill the fields because the label structure is complicated
-      (is (btu/eventually-visible? :fields-0-title-en))
+      (btu/wait-visible :fields-0-title-en)
       (btu/fill-human :fields-0-title-en "Text area (EN)")
       (btu/fill-human :fields-0-title-fi "Text area (FI)")
       (btu/fill-human :fields-0-title-sv "Text area (SV)")
       (btu/scroll-and-click :fields-0-placeholder-more-link)
-      (is (btu/eventually-visible? :fields-0-placeholder-en))
+      (btu/wait-visible :fields-0-placeholder-en)
       (btu/fill-human :fields-0-placeholder-en "Placeholder (EN)")
       (btu/fill-human :fields-0-placeholder-fi "Placeholder (FI)")
       (btu/fill-human :fields-0-placeholder-sv "Placeholder (SV)")
       (btu/scroll-and-click :fields-0-info-text-more-link)
-      (is (btu/eventually-visible? :fields-0-info-text-en))
+      (btu/wait-visible :fields-0-info-text-en)
       (btu/fill-human :fields-0-info-text-en "") ; should not get passed as is blank
       (btu/fill-human :fields-0-info-text-fi " ")
       (btu/fill-human :fields-0-info-text-sv "")
@@ -1150,12 +1146,12 @@
 
       ;; need to filter by visible-el? since there's a leftover invisible :add-form-field from the removed field
       (btu/scroll-and-click-el (last (btu/query-all {:class :add-form-field})))
-      (is (btu/eventually-visible? :fields-1-title-en))
+      (btu/wait-visible :fields-1-title-en)
       (btu/fill-human :fields-1-title-en "Option list (EN)")
       (btu/fill-human :fields-1-title-fi "Option list (FI)")
       (btu/fill-human :fields-1-title-sv "Option list (SV)")
       (btu/scroll-and-click :fields-1-info-text-more-link)
-      (is (btu/eventually-visible? :fields-1-info-text-en))
+      (btu/wait-visible :fields-1-info-text-en)
       (btu/fill-human :fields-1-info-text-en "Info text (EN)")
       (btu/fill-human :fields-1-info-text-fi "Info text (FI)")
       (btu/fill-human :fields-1-info-text-sv "Info text (SV)")
@@ -1164,13 +1160,13 @@
       (btu/fill-human :fields-1-id "opt")
       (btu/scroll-and-click :fields-1-type-option)
       (btu/scroll-and-click {:class :add-option})
-      (is (btu/eventually-visible? :fields-1-options-0-key))
+      (btu/wait-visible :fields-1-options-0-key)
       (btu/fill-human :fields-1-options-0-key "true")
       (btu/fill-human :fields-1-options-0-label-en "Yes")
       (btu/fill-human :fields-1-options-0-label-fi "Kyllä")
       (btu/fill-human :fields-1-options-0-label-sv "Ja")
       (btu/scroll-and-click {:class :add-option})
-      (is (btu/eventually-visible? :fields-1-options-1-key))
+      (btu/wait-visible :fields-1-options-1-key)
       (btu/fill-human :fields-1-options-1-key "false")
       (btu/fill-human :fields-1-options-1-label-en "No")
       (btu/fill-human :fields-1-options-1-label-fi "Ei")
@@ -1182,7 +1178,7 @@
       (btu/scroll-and-click :save))
 
     (testing "view form"
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Form"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Form"})
       (btu/wait-page-loaded)
       (is (= {"Organization" "NBN"
               "Name" "Form editor test"
@@ -1193,7 +1189,7 @@
              (slurp-fields :form)))
       (testing "preview"
         ;; the text is split into multiple DOM nodes so we need btu/has-text?, :fn/has-text is simpler for some reason
-        (is (btu/eventually-visible? {:tag :button :fn/has-class :info-button}))
+        (btu/wait-visible {:tag :button :fn/has-class :info-button})
         (is (btu/has-text? {:tag :label :class :application-field-label :fn/has-text "Text area (EN)"}
                            "(max 127 characters)"))
         (is (btu/has-text? {:tag :label :class :application-field-label :fn/has-text "Text area (EN)"}
@@ -1204,7 +1200,7 @@
         (is (not (btu/visible? {:tag :div :fn/has-class :info-collapse})))
         (is (not (btu/visible? {:tag :div :fn/has-text "Info text (EN)"})))
         (btu/click-el (first (btu/query-all {:tag :button :fn/has-class :info-button})))
-        (is (btu/eventually-visible? {:tag :div :fn/has-class :info-collapse}))
+        (btu/wait-visible {:tag :div :fn/has-class :info-collapse})
         (is (btu/visible? {:tag :div :fn/has-text "Info text (EN)"}))
         ;; TODO: figure out what to wait for
         (Thread/sleep 500)
@@ -1212,17 +1208,17 @@
         (btu/wait-invisible {:tag :div :fn/has-text "Info text (EN)"})
         (btu/wait-predicate #(not (btu/visible? {:tag :div :fn/has-text "Info text (EN)"})) {:timeout 30})
         (change-language :fi)
-        (is (btu/eventually-visible? {:tag :label :class :application-field-label :fn/has-text "Text area (FI)"}))
+        (btu/wait-visible {:tag :label :class :application-field-label :fn/has-text "Text area (FI)"})
         (is (btu/visible? {:tag :label :class :application-field-label :fn/has-text "Text area (FI)"}))
         (btu/click-el (first (btu/query-all {:tag :button :fn/has-class :info-button})))
-        (is (btu/eventually-visible? {:tag :div :fn/has-class :info-collapse}))
+        (btu/wait-visible {:tag :div :fn/has-class :info-collapse})
         (is (btu/visible? {:tag :div :fn/has-text "Info text (FI)"}))))
 
     (testing "edit form"
       (change-language :en)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Form"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Form"})
       (btu/scroll-and-click {:fn/has-class :edit-form})
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Edit form"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Edit form"})
 
       (testing "add description field"
         (btu/scroll-and-click {:class :add-form-field})
@@ -1233,18 +1229,18 @@
 
         (btu/scroll-and-click :save)
         (btu/wait-page-loaded)
-        (is (btu/eventually-visible? {:tag :h1 :fn/has-text "Form"}))
+        (btu/wait-visible {:tag :h1 :fn/has-text "Form"})
         (is (btu/visible? {:tag :label :class :application-field-label :fn/has-text "Option list (EN)"})))
 
       (testing "check that error message is present on field empty"
         (btu/scroll-and-click {:fn/has-class :edit-form})
         (btu/wait-page-loaded)
-        (is (btu/eventually-visible? {:tag :h1 :fn/text "Edit form"}))
+        (btu/wait-visible {:tag :h1 :fn/text "Edit form"})
 
         (btu/scroll-and-click :field-editor-fld2-collapse-more-link)
         (btu/scroll-and-click :fields-0-type-description)
         (btu/scroll-and-click :fields-0-info-text-more-link)
-        (is (btu/eventually-visible? :fields-0-info-text-en))
+        (btu/wait-visible :fields-0-info-text-en)
         (btu/fill-human :fields-0-info-text-en "Info text (EN)")
         (btu/fill-human :fields-0-info-text-fi "Info text (FI)")
         (btu/fill-human :fields-0-info-text-sv " ")
@@ -1252,7 +1248,7 @@
         (btu/scroll-and-click :save)
 
         (btu/wait-page-loaded)
-        (is (btu/eventually-visible? {:tag :h1 :fn/has-text "Edit form"}))
+        (btu/wait-visible {:tag :h1 :fn/has-text "Edit form"})
         (is (btu/visible? {:id :fields-0-info-text-sv :fn/has-class :is-invalid}))
         ;; :fn/has-text has trouble working for the whole "Field \"Field description (optional)\" is required." string
         (is (btu/visible? {:fn/has-class :invalid-feedback :fn/has-text "Field description (optional)"}))
@@ -1263,7 +1259,7 @@
         (btu/fill-human :fields-0-info-text-sv "Info text (SV)")
         (btu/scroll-and-click :save)
         (btu/wait-page-loaded)
-        (is (btu/eventually-visible? {:tag :h1 :fn/has-text "Form"}))))
+        (btu/wait-visible {:tag :h1 :fn/has-text "Form"})))
 
     (testing "fetch form via api"
       (let [form-id (Integer/parseInt (last (str/split (btu/get-url) #"/")))]
@@ -1311,7 +1307,7 @@
     (testing "create workflow"
       (btu/context-assoc! :workflow-title (str "test-workflow-create-edit " (btu/get-seed)))
       (btu/scroll-and-click :create-workflow)
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Create workflow"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Create workflow"})
       (btu/wait-page-loaded)
       (select-option "Organization" "nbn")
       (fill-form-field "Title" (btu/context-get :workflow-title))
@@ -1323,7 +1319,7 @@
       (btu/screenshot "test-workflow-create-edit-1.png")
       (btu/scroll-and-click :save))
     (testing "view workflow"
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Workflow"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Workflow"})
       (btu/wait-page-loaded)
       (btu/screenshot "test-workflow-create-edit-2.png")
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
@@ -1336,7 +1332,7 @@
              (slurp-fields :workflow))))
     (testing "edit workflow"
       (btu/scroll-and-click {:fn/has-class :edit-workflow})
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Edit workflow"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Edit workflow"})
       (btu/wait-page-loaded)
       (btu/screenshot "test-workflow-create-edit-3.png")
       (select-option "Organization" "Default")
@@ -1348,7 +1344,7 @@
       (btu/screenshot "test-workflow-create-edit-4.png")
       (btu/scroll-and-click :save))
     (testing "view workflow again"
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Workflow"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Workflow"})
       (btu/wait-page-loaded)
       (btu/screenshot "test-workflow-create-edit-5.png")
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
@@ -1371,18 +1367,18 @@
       (go-to-admin "Resources")
       (click-row-action [:resources] {:fn/text "blacklist-test"}
                         (select-button-by-label "View"))
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Resource"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Resource"})
       (btu/wait-page-loaded)
-      (is (btu/eventually-visible? :blacklist))
+      (btu/wait-visible :blacklist)
       (is (= [{}] (slurp-rows :blacklist)))
       (btu/fill-human :blacklist-user "baddie\n")
       (btu/fill-human :blacklist-comment "This is a test.")
       (btu/screenshot "test-blacklist-1.png")
       (btu/scroll-and-click :blacklist-add)
-      (is (btu/eventually-visible? {:css ".alert-success"}))
+      (btu/wait-visible {:css ".alert-success"})
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success")))
     (testing "check entry on resource page"
-      (is (btu/eventually-visible? :blacklist))
+      (btu/wait-visible :blacklist)
       (is (= [{} ;; TODO remove the header row in slurp-rows
               {"resource" "blacklist-test"
                "user" "Bruce Baddie"
@@ -1394,9 +1390,9 @@
              (mapv #(dissoc % "added-at") (slurp-rows :blacklist)))))
     (testing "check entry on blacklist page"
       (go-to-admin "Blacklist")
-      (is (btu/eventually-visible? {:tag :h1 :fn/text "Blacklist"}))
+      (btu/wait-visible {:tag :h1 :fn/text "Blacklist"})
       (btu/wait-page-loaded)
-      (is (btu/eventually-visible? :blacklist))
+      (btu/wait-visible :blacklist)
       (is (= [{}
               {"resource" "blacklist-test"
                "user" "Bruce Baddie"
@@ -1409,9 +1405,9 @@
     (testing "remove entry"
       (click-row-action [:blacklist] {:fn/text "baddie"}
                         (select-button-by-label "Remove"))
-      (is (btu/eventually-visible? {:css ".alert-success"}))
+      (btu/wait-visible {:css ".alert-success"})
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
-      (is (btu/eventually-visible? :blacklist))
+      (btu/wait-visible :blacklist)
       (is (= [{}] (slurp-rows :blacklist))))))
 
 (deftest test-report
@@ -1449,7 +1445,7 @@
       (go-to-admin "Reports")
       (btu/scroll-and-click :export-applications-button)
       (btu/wait-page-loaded)
-      (is (btu/eventually-visible? {:tag :label :fn/text "Form"}))
+      (btu/wait-visible {:tag :label :fn/text "Form"})
       (select-option* "Form" (btu/context-get :form-title))
       (btu/scroll-and-click :export-applications-button)
       (btu/wait-for-downloads #"applications_.*\.csv")) ; report has time in it that is difficult to control
@@ -1489,7 +1485,7 @@
       (btu/scroll-and-click :create-organization)
       (btu/context-assoc! :organization-id (str "Organization id " (btu/get-seed)))
       (btu/context-assoc! :organization-name (str "Organization " (btu/get-seed)))
-      (is (btu/eventually-visible? :id))
+      (btu/wait-visible :id)
       (btu/fill-human :id (btu/context-get :organization-id))
       (btu/fill-human :short-name-en "SNEN")
       (btu/fill-human :short-name-fi "SNFI")
@@ -1501,18 +1497,18 @@
       (btu/scroll-and-click :add-review-email)
       (btu/scroll-and-click :add-review-email)
 
-      (is (btu/eventually-visible? :review-emails-1-name-en))
+      (btu/wait-visible :review-emails-1-name-en)
       (btu/fill-human :review-emails-1-name-en "Review mail EN") ; fill second
       (btu/fill-human :review-emails-1-name-fi "Review mail FI")
       (btu/fill-human :review-emails-1-name-sv "Review mail SV")
       (btu/fill-human :review-emails-1-email "review.email@example.com")
       (btu/scroll-and-click {:css ".remove"}) ; remove first
       (btu/scroll-and-click :save)
-      (is (btu/eventually-visible? {:css ".alert-success"}))
+      (btu/wait-visible {:css ".alert-success"})
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success")))
 
     (testing "view after creation"
-      (is (btu/eventually-visible? :organization))
+      (btu/wait-visible :organization)
       (is (= {"Id" (btu/context-get :organization-id)
               "Short name (FI)" "SNFI"
               "Short name (EN)" "SNEN"
@@ -1533,7 +1529,7 @@
     (testing "edit after creation"
       (btu/scroll-and-click :edit-organization)
       (btu/wait-page-loaded)
-      (is (btu/eventually-visible? :short-name-en))
+      (btu/wait-visible :short-name-en)
       (select-option* "Owners" "Organization owner 2")
       (btu/clear :short-name-en)
       (btu/fill-human :short-name-en "SNEN2")
@@ -1542,11 +1538,11 @@
       (btu/clear :short-name-sv)
       (btu/fill-human :short-name-sv "SNSV2")
       (btu/scroll-and-click :save)
-      (is (btu/eventually-visible? {:css ".alert-success"}))
+      (btu/wait-visible {:css ".alert-success"})
       (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
 
       (testing "view after editing"
-        (is (btu/eventually-visible? :organization))
+        (btu/wait-visible :organization)
         (is (= {"Id" (btu/context-get :organization-id)
                 "Short name (FI)" "SNFI2"
                 "Short name (EN)" "SNEN2"
@@ -1569,11 +1565,11 @@
       (btu/wait-page-loaded)
       (btu/scroll-and-click :create-resource)
       (btu/wait-page-loaded)
-      (is (btu/eventually-visible? :organization))
+      (btu/wait-visible :organization)
       (btu/fill-human :resid (str "resource for " (btu/context-get :organization-name)))
       (select-option* "Organization" (btu/context-get :organization-name))
       (btu/scroll-and-click :save)
-      (is (btu/eventually-visible? {:css ".alert-success"})))
+      (btu/wait-visible {:css ".alert-success"}))
 
     (testing "as organization owner"
       (logout)
@@ -1581,7 +1577,7 @@
       (go-to-admin "Organizations")
 
       (testing "list shows created organization"
-        (is (btu/eventually-visible? :organizations))
+        (btu/wait-visible :organizations)
         (let [orgs (slurp-rows :organizations)]
           (is (some #{{"short-name" "SNEN2"
                        "name" (str (btu/context-get :organization-name) " EN")
@@ -1594,7 +1590,7 @@
                           {:fn/text (str (btu/context-get :organization-name) " EN")}
                           (select-button-by-label "View"))
         (btu/wait-page-loaded)
-        (is (btu/eventually-visible? :organization))
+        (btu/wait-visible :organization)
         (is (= {"Id" (btu/context-get :organization-id)
                 "Short name (FI)" "SNFI2"
                 "Short name (EN)" "SNEN2"
@@ -1615,7 +1611,7 @@
       (testing "edit as organization owner"
         (btu/scroll-and-click :edit-organization)
         (btu/wait-page-loaded)
-        (is (btu/eventually-visible? :short-name-en))
+        (btu/wait-visible :short-name-en)
         (btu/clear :short-name-en)
         (btu/fill-human :short-name-en "SNEN")
         (btu/clear :short-name-fi)
@@ -1623,11 +1619,11 @@
         (btu/clear :short-name-sv)
         (btu/fill-human :short-name-sv "SNSV")
         (btu/scroll-and-click :save)
-        (is (btu/eventually-visible? {:css ".alert-success"}))
+        (btu/wait-visible {:css ".alert-success"})
         (is (str/includes? (btu/get-element-text {:css ".alert-success"}) "Success"))
 
         (testing "view after editing"
-          (is (btu/eventually-visible? :organization))
+          (btu/wait-visible :organization)
           (is (= {"Id" (btu/context-get :organization-id)
                   "Short name (FI)" "SNFI"
                   "Short name (EN)" "SNEN"
@@ -1663,14 +1659,14 @@
     (btu/set-window-size 400 600) ; small enough for mobile
     (btu/wait-invisible :small-navbar)
     (btu/scroll-and-click {:css ".navbar-toggler"})
-    (is (btu/eventually-visible? :small-navbar))
+    (btu/wait-visible :small-navbar)
     (btu/screenshot "small-navbar.png")
     (btu/scroll-and-click [:small-navbar {:tag :a :fn/text "Applications"}])
     (btu/wait-invisible :small-navbar) ; menu should be hidden
-    (is (btu/eventually-visible? {:tag :h1 :fn/text "Applications"}))
+    (btu/wait-visible {:tag :h1 :fn/text "Applications"})
     (btu/scroll-and-click {:css ".navbar-toggler"})
     (btu/scroll-and-click [:small-navbar {:tag :button :fn/text "FI"}])
     (btu/wait-invisible :small-navbar) ; menu should be hidden
-    (is (btu/eventually-visible? {:tag :h1 :fn/text "Hakemukset"}))
+    (btu/wait-visible {:tag :h1 :fn/text "Hakemukset"})
     (user-settings/delete-user-settings! "alice") ; clear language settings
     (is true)))  ; avoid no assertions warning
diff --git a/test/clj/rems/test_form_validation.clj b/test/clj/rems/test_form_validation.clj
index 6d7a74a9a..8edec4cc5 100644
--- a/test/clj/rems/test_form_validation.clj
+++ b/test/clj/rems/test_form_validation.clj
@@ -139,67 +139,6 @@
                                          :field/visible  true
                                          :field/value    ""}]))))
 
-  (testing "error: multiselect validation"
-    (is (= nil
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    ""}])
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "ye"}])
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "no"}])
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "ye no xx"}])))
-    (is (= [{:type :t.form.validation/invalid-value :field-id "1"}]
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "foo"}])
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional true
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    "ye foo xx"}])))
-    (is (= [{:type :t.form.validation/required :field-id "1"}]
-           (validate-fields-for-submit [{:field/id       "1"
-                                         :field/type     :multiselect
-                                         :field/visible  true
-                                         :field/optional false
-                                         :field/options  [{:key   "ye"}
-                                                          {:key   "no"}
-                                                          {:key   "xx"}]
-                                         :field/value    ""}]))))
-
   (testing "error: field input invalid email address"
     (is (= [{:type :t.form.validation/required :field-id "1"}
             {:type :t.form.validation/invalid-email :field-id "2"}
@@ -389,46 +328,6 @@
                                         :field/visible  true
                                         :field/value    ""}]))))
 
-  (testing "error: multiselect validation"
-    (is (= nil
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    ""}])
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "ye"}])
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "no"}])
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "ye no xx"}])))
-    (is (= [{:type :t.form.validation/invalid-value :field-id "1"}]
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "foo"}])
-           (validate-fields-for-draft [{:field/id       "1"
-                                        :field/type     :multiselect
-                                        :field/options  [{:key   "ye"}
-                                                         {:key   "no"}
-                                                         {:key   "xx"}]
-                                        :field/value    "ye foo xx"}]))))
-
   (testing "error: field input invalid email address"
     (is (= [{:type :t.form.validation/invalid-email :field-id "2"}
             {:type :t.form.validation/invalid-email :field-id "5"}]
diff --git a/test/clj/rems/test_text.clj b/test/clj/rems/test_text.clj
index 76d6f32f0..6a45901d1 100644
--- a/test/clj/rems/test_text.clj
+++ b/test/clj/rems/test_text.clj
@@ -67,14 +67,6 @@
                               :application/member {:userid "User"
                                                    :email "user@example.com"
                                                    :name "User Urquhart"}})))
-      (is (= "Bob Bond changed the applicant to User Urquhart."
-             (localize-event {:event/type :application.event/applicant-changed
-                              :event/actor "bob"
-                              :event/actor-attributes {:userid "bob"
-                                                       :name "Bob Bond"}
-                              :application/applicant {:userid "User"
-                                                      :email "user@example.com"
-                                                      :name "User Urquhart"}})))
       (is (= "Bob Bond invited User Urquhart <user@example.com> to the application."
              (localize-event {:event/type :application.event/member-invited
                               :event/actor "bob"
diff --git a/test/clj/rems/test_themes.clj b/test/clj/rems/test_themes.clj
index 3f844a640..da2489019 100644
--- a/test/clj/rems/test_themes.clj
+++ b/test/clj/rems/test_themes.clj
@@ -32,9 +32,9 @@
       (is (= "example-theme/public"
              (:theme-static-resources (config/load-external-theme config)))))))
 
-(deftest theme-getx-test
+(deftest get-theme-attribute-test
   (with-redefs [config/env {:theme {:test "success"
                                     :test-color 2}}]
-    (is (= 2 (style-utils/theme-getx :test-color)))
-    (is (= "success" (style-utils/theme-getx :test)))
-    (is (nil? (style-utils/theme-getx :no-such-attribute)))))
+    (is (= 2 (style-utils/get-theme-attribute :test-color)))
+    (is (= "success" (style-utils/get-theme-attribute :test)))
+    (is (nil? (style-utils/get-theme-attribute :no-such-attribute)))))
diff --git a/test/cljs/rems/test_util.cljs b/test/cljs/rems/test_util.cljs
index 861373d3b..384954c61 100644
--- a/test/cljs/rems/test_util.cljs
+++ b/test/cljs/rems/test_util.cljs
@@ -3,7 +3,7 @@
             [cljs-time.core :as time]
             [cljs-time.format :as format]
             [rems.text :refer [time-format localize-time]]
-            [rems.util :refer [linkify]]))
+            [rems.util :refer [decode-option-keys encode-option-keys normalize-option-key linkify]]))
 
 (def test-time #inst "1980-01-02T13:45:00.000Z")
 
@@ -19,6 +19,24 @@
   (is (= (expected-time test-time) (localize-time #inst "1980-01-02T13:45:00.000Z")))
   (is (= nil (localize-time nil))))
 
+(deftest option-keys-test
+  (testing "whitespace is not allowed in a key"
+    (is (= "foo" (normalize-option-key " f o o "))))
+  (testing "encoding"
+    (is (= "" (encode-option-keys #{})))
+    (is (= "foo" (encode-option-keys #{"foo"})))
+    (is (= "bar foo" (encode-option-keys #{"foo" "bar"})))
+    (is (= "bar foo" (encode-option-keys #{"bar" "foo"}))))
+  (testing "decoding"
+    (is (= #{} (decode-option-keys "")))
+    (is (= #{"foo"} (decode-option-keys "foo")))
+    (is (= #{"foo" "bar"} (decode-option-keys "foo bar")))
+    (is (= #{"foo" "bar"} (decode-option-keys "  foo  bar  "))))
+  (testing "round-trip"
+    (is (= #{} (decode-option-keys (encode-option-keys #{}))))
+    (is (= #{"foo"} (decode-option-keys (encode-option-keys #{"foo"}))))
+    (is (= #{"foo" "bar"} (decode-option-keys (encode-option-keys #{"foo" "bar"}))))))
+
 (deftest test-linkify
   (let [link [:a {:target :_blank :href "http://www.abc.com"} "http://www.abc.com"]]
     (testing "retain original string"