From af0543330ece385011d44f0dc3f80e156dad842f Mon Sep 17 00:00:00 2001
From: Bastien Abadie <abadie@teklia.com>
Date: Wed, 28 Feb 2024 13:53:27 +0000
Subject: [PATCH] Community Version

---
 Makefile                                      |    7 +-
 README.md                                     |    5 +
 arkindex/documents/api/elements.py            |   11 +-
 arkindex/documents/fixtures/data.json         | 1962 ++++++------
 .../documents/tests/test_allowed_metadata.py  |  124 +-
 .../tests/test_bulk_classification.py         |   26 +-
 .../tests/test_bulk_element_transcriptions.py |   41 +-
 .../documents/tests/test_bulk_elements.py     |   30 +-
 .../tests/test_bulk_transcription_entities.py |   20 +-
 .../tests/test_bulk_transcriptions.py         |   17 +-
 arkindex/documents/tests/test_classes.py      |   13 +-
 .../documents/tests/test_classification.py    |   50 +-
 arkindex/documents/tests/test_corpus.py       |   25 +-
 .../tests/test_corpus_authorized_users.py     |    5 +
 .../documents/tests/test_create_elements.py   |   54 +-
 .../tests/test_create_parent_selection.py     |   19 +-
 .../tests/test_create_transcriptions.py       |   43 +-
 .../documents/tests/test_destroy_elements.py  |   53 +-
 .../tests/test_destroy_worker_results.py      |   55 +-
 .../tests/test_edit_transcriptions.py         |   19 +-
 .../documents/tests/test_element_paths_api.py |   14 +-
 arkindex/documents/tests/test_element_type.py |   52 +-
 arkindex/documents/tests/test_entities_api.py |  200 +-
 arkindex/documents/tests/test_entity_types.py |  134 +-
 arkindex/documents/tests/test_export.py       |   64 +-
 arkindex/documents/tests/test_metadata.py     |  158 +-
 arkindex/documents/tests/test_move_element.py |   19 +-
 .../documents/tests/test_move_selection.py    |   19 +-
 arkindex/documents/tests/test_neighbors.py    |   15 +-
 .../documents/tests/test_patch_elements.py    |   21 +-
 arkindex/documents/tests/test_put_elements.py |   24 +-
 .../documents/tests/test_retrieve_elements.py |   16 +-
 arkindex/documents/tests/test_search_api.py   |   26 +-
 .../documents/tests/test_selection_api.py     |   17 +-
 .../documents/tests/test_transcriptions.py    |   34 +-
 arkindex/images/tests/test_image_elements.py  |   20 +-
 arkindex/ponos/admin.py                       |  163 +-
 arkindex/ponos/api.py                         |  287 +-
 arkindex/ponos/authentication.py              |   68 +-
 arkindex/ponos/keys.py                        |  112 -
 arkindex/ponos/management/__init__.py         |    0
 .../ponos/management/commands/__init__.py     |    0
 .../commands/generate_private_key.py          |   19 -
 arkindex/ponos/migrations/0001_initial.py     |   14 +-
 arkindex/ponos/models.py                      |  253 +-
 arkindex/ponos/renderers.py                   |   19 -
 arkindex/ponos/serializers.py                 |  392 +--
 arkindex/ponos/tasks.py                       |  161 +
 arkindex/ponos/tests/test_admin.py            |   47 -
 arkindex/ponos/tests/test_api.py              | 2753 ++---------------
 arkindex/ponos/tests/test_artifacts_api.py    |  152 +-
 arkindex/ponos/tests/test_keys.py             |   89 -
 arkindex/ponos/tests/test_models.py           |   96 +-
 arkindex/ponos/tests/test_rq_tasks.py         |   53 +
 .../ponos/tests/test_tasks_attribution.py     |  481 ---
 arkindex/ponos/utils.py                       |   14 +
 arkindex/process/admin.py                     |    5 +-
 arkindex/process/api.py                       |    8 +-
 arkindex/process/builder.py                   |    3 +-
 arkindex/process/models.py                    |    6 +
 arkindex/process/serializers/imports.py       |   13 +-
 arkindex/process/serializers/ingest.py        |    2 +-
 .../process/tests/test_corpus_worker_runs.py  |   11 +-
 arkindex/process/tests/test_create_process.py |   42 +-
 .../process/tests/test_create_s3_import.py    |   40 +-
 arkindex/process/tests/test_datafile_api.py   |    3 +-
 .../process/tests/test_process_datasets.py    |  128 +-
 .../process/tests/test_process_elements.py    |   19 +-
 arkindex/process/tests/test_processes.py      |  243 +-
 arkindex/process/tests/test_repos.py          |   35 +-
 arkindex/process/tests/test_signals.py        |  188 +-
 arkindex/process/tests/test_templates.py      |   34 +-
 .../process/tests/test_user_workerruns.py     |   50 +-
 arkindex/process/tests/test_utils.py          |   67 -
 .../tests/test_worker_configurations.py       |  291 +-
 arkindex/process/tests/test_workeractivity.py |    7 +-
 .../tests/test_workeractivity_stats.py        |   17 +-
 arkindex/process/tests/test_workerruns.py     |  240 +-
 arkindex/process/tests/test_workers.py        |  433 ++-
 arkindex/process/utils.py                     |   20 +-
 arkindex/project/api_v1.py                    |   45 +-
 arkindex/project/checks.py                    |   17 -
 arkindex/project/config.py                    |    5 +-
 arkindex/project/mixins.py                    |   30 +-
 arkindex/project/rq_overrides.py              |    2 +-
 arkindex/project/serializer_fields.py         |   11 +
 arkindex/project/settings.py                  |   35 +-
 .../tests/config_samples/defaults.yaml        |    3 +-
 .../project/tests/config_samples/errors.yaml  |    1 +
 .../tests/config_samples/expected_errors.yaml |    1 +
 .../tests/config_samples/override.yaml        |    5 +-
 arkindex/project/tests/openapi/test_schema.py |    1 -
 arkindex/project/tests/test_acl_mixin.py      |  128 +-
 arkindex/project/tools.py                     |   21 -
 arkindex/project/triggers.py                  |   15 +
 arkindex/sql_validation/corpus_delete.sql     |    2 +-
 .../corpus_delete_top_level_type.sql          |    2 +-
 arkindex/training/api.py                      |    4 +-
 arkindex/training/tests/test_datasets_api.py  |  284 +-
 arkindex/training/tests/test_metrics_api.py   |   67 +-
 arkindex/training/tests/test_model_api.py     |  401 +--
 .../tests/test_model_compatible_worker.py     |  133 +-
 arkindex/users/admin.py                       |   36 +-
 arkindex/users/allow_all.py                   |   28 +
 arkindex/users/api.py                         |  332 +-
 arkindex/users/serializers.py                 |  202 +-
 .../users/tests/test_generic_memberships.py   | 1078 -------
 arkindex/users/tests/test_manager_acl.py      |   49 -
 arkindex/users/tests/test_registration.py     |   27 +-
 arkindex/users/utils.py                       |  123 +-
 requirements.txt                              |    2 +
 111 files changed, 3852 insertions(+), 9728 deletions(-)
 delete mode 100644 arkindex/ponos/keys.py
 delete mode 100644 arkindex/ponos/management/__init__.py
 delete mode 100644 arkindex/ponos/management/commands/__init__.py
 delete mode 100644 arkindex/ponos/management/commands/generate_private_key.py
 delete mode 100644 arkindex/ponos/renderers.py
 delete mode 100644 arkindex/ponos/tests/test_admin.py
 delete mode 100644 arkindex/ponos/tests/test_keys.py
 create mode 100644 arkindex/ponos/tests/test_rq_tasks.py
 delete mode 100644 arkindex/ponos/tests/test_tasks_attribution.py
 delete mode 100644 arkindex/process/tests/test_utils.py
 create mode 100644 arkindex/users/allow_all.py
 delete mode 100644 arkindex/users/tests/test_generic_memberships.py
 delete mode 100644 arkindex/users/tests/test_manager_acl.py

diff --git a/Makefile b/Makefile
index 93765fb75c..dd13c5dd74 100644
--- a/Makefile
+++ b/Makefile
@@ -13,13 +13,10 @@ clean:
 	find . -name '*.pyc' -exec rm {} \;
 
 build:
-	CI_PROJECT_DIR=$(ROOT_DIR) CI_REGISTRY_IMAGE=$(IMAGE_TAG) $(ROOT_DIR)/ci/build.sh Dockerfile
-
-binary:
-	CI_PROJECT_DIR=$(ROOT_DIR) CI_REGISTRY_IMAGE=$(IMAGE_TAG) $(ROOT_DIR)/ci/build.sh Dockerfile.binary -binary
+	CI_PROJECT_DIR=$(ROOT_DIR) CI_REGISTRY_IMAGE=$(IMAGE_TAG) $(ROOT_DIR)/ci/build.sh
 
 worker:
-	arkindex/manage.py rqworker -v 2 default high
+	arkindex/manage.py rqworker -v 2 default high tasks
 
 test-fixtures:
 	$(eval export PGPASSWORD=devdata)
diff --git a/README.md b/README.md
index 2721513724..1209bef98d 100644
--- a/README.md
+++ b/README.md
@@ -160,6 +160,11 @@ We use [rq](https://python-rq.org/), integrated via [django-rq](https://pypi.org
 
 To run them, use `make worker` to start a RQ worker. You will need to have Redis running; `make slim` or `make` in the architecture will provide it. `make` in the architecture also provides a RQ worker running in Docker from a binary build.
 
+Process tasks are run in RQ by default (Community Edition). Two RQ workers must be running at the same time to actually run a process with worker activities, so the initialisation task can wait for the worker activity task to finish:
+```sh
+$ manage.py rqworker -v 3 default high & manage.py rqworker -v 3 tasks
+```
+
 ## Metrics
 The application serves metrics for Prometheus under the `/metrics` prefix.
 A specific port can be used by setting the `PROMETHEUS_METRICS_PORT` environment variable, thus separating the application from the metrics API.
diff --git a/arkindex/documents/api/elements.py b/arkindex/documents/api/elements.py
index 6b5f1925ba..141794ab37 100644
--- a/arkindex/documents/api/elements.py
+++ b/arkindex/documents/api/elements.py
@@ -1303,17 +1303,16 @@ class ElementNeighbors(ACLMixin, ListAPIView):
     queryset = Element.objects.none()
 
     def get_queryset(self):
-
         element = get_object_or_404(
             # Include the attributes required for ACL checks and the API response
-            Element.objects.select_related("corpus", "type").only("id", "name", "type__slug", "corpus__public"),
+            Element
+            .objects
+            .filter(corpus__in=Corpus.objects.readable(self.request.user))
+            .select_related("corpus", "type")
+            .only("id", "name", "type__slug", "corpus__public"),
             id=self.kwargs["pk"]
         )
 
-        # Check access permission
-        if not self.has_access(element.corpus, Role.Guest.value):
-            raise PermissionDenied(detail="You do not have a read access to this element.")
-
         return Element.objects.get_neighbors(element)
 
 
diff --git a/arkindex/documents/fixtures/data.json b/arkindex/documents/fixtures/data.json
index 20d3e109a8..9e60c75219 100644
--- a/arkindex/documents/fixtures/data.json
+++ b/arkindex/documents/fixtures/data.json
@@ -1,19 +1,19 @@
 [
 {
     "model": "process.process",
-    "pk": "245bc206-350c-43d5-8db3-d98645c4eaa9",
+    "pk": "26050593-712f-4f7a-b15a-4d2132241514",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "name": null,
-        "creator": 1,
-        "corpus": null,
-        "mode": "repository",
+        "name": "Process fixture",
+        "creator": 2,
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "mode": "workers",
         "revision": null,
         "activity_state": "disabled",
         "started": null,
         "finished": null,
-        "farm": "c19116b6-866f-4bb7-b447-3544629a8151",
+        "farm": "94055f35-9ac9-44a0-ac5d-386e132bea2c",
         "element": null,
         "folder_type": null,
         "element_type": null,
@@ -32,19 +32,19 @@
 },
 {
     "model": "process.process",
-    "pk": "64b06ffd-a8c4-4ba2-bbb5-c99eb99121a0",
+    "pk": "3421ba72-b14c-4df0-a504-1e7e90abe4b4",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "name": "Process fixture",
-        "creator": 2,
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "mode": "workers",
+        "name": null,
+        "creator": 1,
+        "corpus": null,
+        "mode": "repository",
         "revision": null,
         "activity_state": "disabled",
         "started": null,
         "finished": null,
-        "farm": "c19116b6-866f-4bb7-b447-3544629a8151",
+        "farm": "94055f35-9ac9-44a0-ac5d-386e132bea2c",
         "element": null,
         "folder_type": null,
         "element_type": null,
@@ -63,7 +63,7 @@
 },
 {
     "model": "process.process",
-    "pk": "98e30036-bd74-4480-82c0-98c3199a4503",
+    "pk": "4a3c1857-7fe7-47ba-9e47-7ce96eefc96f",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
@@ -94,7 +94,7 @@
 },
 {
     "model": "process.process",
-    "pk": "e395a111-ab5a-4c93-8901-b1f8bafde684",
+    "pk": "be97d04e-0af4-4043-9874-d20f28b30431",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
@@ -125,182 +125,184 @@
 },
 {
     "model": "process.repository",
-    "pk": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+    "pk": "0e1654c4-20e5-4d4a-99ab-7142040b6e88",
     "fields": {
-        "url": "http://my_repo.fake/workers/worker"
+        "url": "http://gitlab/repo"
     }
 },
 {
     "model": "process.repository",
-    "pk": "b44445fb-1586-4673-b61c-c8057ccc9878",
+    "pk": "a091592f-0ea9-4147-be4e-4f580751ac9f",
     "fields": {
-        "url": "http://gitlab/repo"
+        "url": "http://my_repo.fake/workers/worker"
     }
 },
 {
     "model": "process.revision",
-    "pk": "5f17c014-45e6-416e-9a6e-86a5de0f79c5",
+    "pk": "539e2eb4-6180-4d45-bc2f-ddd4bbe08ab1",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "repo": "b44445fb-1586-4673-b61c-c8057ccc9878",
-        "hash": "42",
-        "message": "Salve",
-        "author": "Some user"
+        "repo": "a091592f-0ea9-4147-be4e-4f580751ac9f",
+        "hash": "1337",
+        "message": "My w0rk3r",
+        "author": "Test user"
     }
 },
 {
     "model": "process.revision",
-    "pk": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+    "pk": "d2d52ec9-2d56-4df1-9118-ed884f26cb83",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "repo": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
-        "hash": "1337",
-        "message": "My w0rk3r",
-        "author": "Test user"
+        "repo": "0e1654c4-20e5-4d4a-99ab-7142040b6e88",
+        "hash": "42",
+        "message": "Salve",
+        "author": "Some user"
     }
 },
 {
     "model": "process.worker",
-    "pk": "19c68046-9fde-4aa6-804d-d3888205722a",
+    "pk": "5a6c0c92-b9b1-4a69-a8d0-b75841cd0193",
     "fields": {
-        "name": "Generic worker with a Model",
-        "slug": "generic",
-        "type": "189c3b32-1172-4a79-b9a2-3cb14f7f52e3",
+        "name": "File import",
+        "slug": "file_import",
+        "type": "2c4d9006-9c1f-4a62-944d-a8862aa022b1",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": "a091592f-0ea9-4147-be4e-4f580751ac9f",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "1ab3c191-f306-4c6b-89b5-5d41a50a3d22",
+    "pk": "7458bec4-665b-4e2d-862f-7d87bc833481",
     "fields": {
-        "name": "Document layout analyser",
-        "slug": "dla",
-        "type": "c804b4ef-b2ed-4b82-90c6-fb020e3542d8",
+        "name": "Custom worker",
+        "slug": "custom",
+        "type": "9785b33d-d517-4a91-a4d9-b192e6b042b6",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": null,
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "5a12340d-c2b9-4d28-800b-6563fb9f6bec",
+    "pk": "89964ea9-97a3-4edf-8568-545ea810342e",
     "fields": {
-        "name": "Recognizer",
-        "slug": "reco",
-        "type": "189c3b32-1172-4a79-b9a2-3cb14f7f52e3",
+        "name": "Document layout analyser",
+        "slug": "dla",
+        "type": "3b3adc81-73f1-4e92-8093-d1dd9431c53f",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": "a091592f-0ea9-4147-be4e-4f580751ac9f",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "62666365-d37d-4e30-91ad-723405dbdc8e",
+    "pk": "c7439e52-9b09-489b-8dc5-13994801e8ac",
     "fields": {
         "name": "Worker requiring a GPU",
         "slug": "worker-gpu",
-        "type": "429e7774-99cd-4672-9438-7d576a69a1a4",
+        "type": "4696efe7-e3dd-4a8c-8346-c99d479c1756",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": "a091592f-0ea9-4147-be4e-4f580751ac9f",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "bf76d80a-ca52-47e2-8670-2d60c1a675b0",
+    "pk": "c88cf97b-25a1-4074-a88c-8d2b7a82db8c",
     "fields": {
-        "name": "Custom worker",
-        "slug": "custom",
-        "type": "2b2c3dae-98be-42c3-927b-28440f8df5d0",
+        "name": "Recognizer",
+        "slug": "reco",
+        "type": "999707eb-4c53-469d-8fb6-ec6a80010248",
         "description": "",
-        "repository": null,
+        "repository": "a091592f-0ea9-4147-be4e-4f580751ac9f",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "e61fde1d-e5bb-4188-aefa-1ceb04fa34a7",
+    "pk": "f62b2ea5-7bff-4c38-9bd1-7237543cad36",
     "fields": {
-        "name": "File import",
-        "slug": "file_import",
-        "type": "2164e147-3109-4a68-93d7-d68341dedfb2",
+        "name": "Generic worker with a Model",
+        "slug": "generic",
+        "type": "999707eb-4c53-469d-8fb6-ec6a80010248",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": "a091592f-0ea9-4147-be4e-4f580751ac9f",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.workertype",
-    "pk": "189c3b32-1172-4a79-b9a2-3cb14f7f52e3",
+    "pk": "2c4d9006-9c1f-4a62-944d-a8862aa022b1",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "slug": "recognizer",
-        "display_name": "Recognizer"
+        "slug": "import",
+        "display_name": "Import"
     }
 },
 {
     "model": "process.workertype",
-    "pk": "2164e147-3109-4a68-93d7-d68341dedfb2",
+    "pk": "3b3adc81-73f1-4e92-8093-d1dd9431c53f",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "slug": "import",
-        "display_name": "Import"
+        "slug": "dla",
+        "display_name": "Document Layout Analysis"
     }
 },
 {
     "model": "process.workertype",
-    "pk": "2b2c3dae-98be-42c3-927b-28440f8df5d0",
+    "pk": "4696efe7-e3dd-4a8c-8346-c99d479c1756",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "slug": "custom",
-        "display_name": "Custom"
+        "slug": "worker",
+        "display_name": "Worker requiring a GPU"
     }
 },
 {
     "model": "process.workertype",
-    "pk": "429e7774-99cd-4672-9438-7d576a69a1a4",
+    "pk": "9785b33d-d517-4a91-a4d9-b192e6b042b6",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "slug": "worker",
-        "display_name": "Worker requiring a GPU"
+        "slug": "custom",
+        "display_name": "Custom"
     }
 },
 {
     "model": "process.workertype",
-    "pk": "c804b4ef-b2ed-4b82-90c6-fb020e3542d8",
+    "pk": "999707eb-4c53-469d-8fb6-ec6a80010248",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "slug": "dla",
-        "display_name": "Document Layout Analysis"
+        "slug": "recognizer",
+        "display_name": "Recognizer"
     }
 },
 {
     "model": "process.workerversion",
-    "pk": "1e0dee90-0973-4017-b23d-4d972d7cfc09",
+    "pk": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
     "fields": {
-        "worker": "e61fde1d-e5bb-4188-aefa-1ceb04fa34a7",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "c88cf97b-25a1-4074-a88c-8d2b7a82db8c",
+        "revision": "539e2eb4-6180-4d45-bc2f-ddd4bbe08ab1",
         "version": null,
-        "configuration": {},
+        "configuration": {
+            "test": 42
+        },
         "state": "available",
         "gpu_usage": "disabled",
         "model_usage": "disabled",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "docker_image": "492ea45f-2c2d-4bf3-a461-016204fcfb03",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -308,9 +310,9 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "5ce2dcd8-f35e-41d1-bd73-b40c9661db77",
+    "pk": "23473bff-4362-466e-ba5f-b6843df1fdf3",
     "fields": {
-        "worker": "bf76d80a-ca52-47e2-8670-2d60c1a675b0",
+        "worker": "7458bec4-665b-4e2d-862f-7d87bc833481",
         "revision": null,
         "version": 1,
         "configuration": {
@@ -327,18 +329,18 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+    "pk": "2aa45507-c5f2-4e1f-bc38-edf50dbf5aae",
     "fields": {
-        "worker": "5a12340d-c2b9-4d28-800b-6563fb9f6bec",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "f62b2ea5-7bff-4c38-9bd1-7237543cad36",
+        "revision": "539e2eb4-6180-4d45-bc2f-ddd4bbe08ab1",
         "version": null,
         "configuration": {
             "test": 42
         },
         "state": "available",
         "gpu_usage": "disabled",
-        "model_usage": "disabled",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "model_usage": "required",
+        "docker_image": "492ea45f-2c2d-4bf3-a461-016204fcfb03",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -346,10 +348,10 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "79d83370-d164-4052-bb29-18f475916ea4",
+    "pk": "66d545f5-4f60-436a-9cc3-517504cce152",
     "fields": {
-        "worker": "62666365-d37d-4e30-91ad-723405dbdc8e",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "c7439e52-9b09-489b-8dc5-13994801e8ac",
+        "revision": "539e2eb4-6180-4d45-bc2f-ddd4bbe08ab1",
         "version": null,
         "configuration": {
             "test": 42
@@ -357,7 +359,7 @@
         "state": "available",
         "gpu_usage": "required",
         "model_usage": "disabled",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "docker_image": "492ea45f-2c2d-4bf3-a461-016204fcfb03",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -365,18 +367,18 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "9e43ea6a-3f53-4a54-9a66-8f1699570f59",
+    "pk": "7e54d670-f1f2-4b96-a38a-ac6a5416ea72",
     "fields": {
-        "worker": "19c68046-9fde-4aa6-804d-d3888205722a",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "89964ea9-97a3-4edf-8568-545ea810342e",
+        "revision": "539e2eb4-6180-4d45-bc2f-ddd4bbe08ab1",
         "version": null,
         "configuration": {
             "test": 42
         },
         "state": "available",
         "gpu_usage": "disabled",
-        "model_usage": "required",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "model_usage": "disabled",
+        "docker_image": "492ea45f-2c2d-4bf3-a461-016204fcfb03",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -384,18 +386,16 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "b0db756e-b291-4ffe-99c7-cd347741d7db",
+    "pk": "88a1c9ce-3128-4219-a120-9ea9574316a3",
     "fields": {
-        "worker": "1ab3c191-f306-4c6b-89b5-5d41a50a3d22",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "5a6c0c92-b9b1-4a69-a8d0-b75841cd0193",
+        "revision": "539e2eb4-6180-4d45-bc2f-ddd4bbe08ab1",
         "version": null,
-        "configuration": {
-            "test": 42
-        },
+        "configuration": {},
         "state": "available",
         "gpu_usage": "disabled",
         "model_usage": "disabled",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "docker_image": "492ea45f-2c2d-4bf3-a461-016204fcfb03",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -403,14 +403,14 @@
 },
 {
     "model": "process.workerrun",
-    "pk": "4b931fc3-4c2b-44ea-8743-4907915554bb",
+    "pk": "796a41f5-9797-443d-be13-ea73372e4950",
     "fields": {
-        "process": "64b06ffd-a8c4-4ba2-bbb5-c99eb99121a0",
-        "version": "b0db756e-b291-4ffe-99c7-cd347741d7db",
+        "process": "4a3c1857-7fe7-47ba-9e47-7ce96eefc96f",
+        "version": "23473bff-4362-466e-ba5f-b6843df1fdf3",
         "model_version": null,
         "parents": "[]",
         "configuration": null,
-        "summary": "Worker Document layout analyser @ b0db75",
+        "summary": "Worker Custom worker @ version 1",
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "has_results": false
@@ -418,14 +418,14 @@
 },
 {
     "model": "process.workerrun",
-    "pk": "4d26aa77-969b-4734-9260-a52b63d6a1d6",
+    "pk": "9b99e704-47e6-478c-988f-323c636bb91a",
     "fields": {
-        "process": "64b06ffd-a8c4-4ba2-bbb5-c99eb99121a0",
-        "version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "process": "26050593-712f-4f7a-b15a-4d2132241514",
+        "version": "7e54d670-f1f2-4b96-a38a-ac6a5416ea72",
         "model_version": null,
-        "parents": "[\"4b931fc3-4c2b-44ea-8743-4907915554bb\"]",
+        "parents": "[]",
         "configuration": null,
-        "summary": "Worker Recognizer @ 70b6b8",
+        "summary": "Worker Document layout analyser @ 7e54d6",
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "has_results": false
@@ -433,14 +433,14 @@
 },
 {
     "model": "process.workerrun",
-    "pk": "83959955-af5d-459f-8e33-47b39e256852",
+    "pk": "810d22b3-90bf-44bb-a931-9c20d6b600ca",
     "fields": {
-        "process": "e395a111-ab5a-4c93-8901-b1f8bafde684",
-        "version": "5ce2dcd8-f35e-41d1-bd73-b40c9661db77",
+        "process": "26050593-712f-4f7a-b15a-4d2132241514",
+        "version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "model_version": null,
-        "parents": "[]",
+        "parents": "[\"9b99e704-47e6-478c-988f-323c636bb91a\"]",
         "configuration": null,
-        "summary": "Worker Custom worker @ version 1",
+        "summary": "Worker Recognizer @ 06e217",
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "has_results": false
@@ -448,10 +448,10 @@
 },
 {
     "model": "process.workerrun",
-    "pk": "86240072-39ba-4597-a7a5-9078f3134657",
+    "pk": "9c5e8955-084a-4dd4-a816-c74d2ac0ab49",
     "fields": {
-        "process": "98e30036-bd74-4480-82c0-98c3199a4503",
-        "version": "5ce2dcd8-f35e-41d1-bd73-b40c9661db77",
+        "process": "be97d04e-0af4-4043-9874-d20f28b30431",
+        "version": "23473bff-4362-466e-ba5f-b6843df1fdf3",
         "model_version": null,
         "parents": "[]",
         "configuration": null,
@@ -463,7 +463,7 @@
 },
 {
     "model": "documents.corpus",
-    "pk": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+    "pk": "1204ee59-2b68-4dfb-a083-3df4996c2337",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
@@ -476,11 +476,11 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
+    "pk": "097f73b9-4ea7-4063-98b0-e17eb698e77a",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "word",
-        "display_name": "Word",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "slug": "text_line",
+        "display_name": "Line",
         "folder": false,
         "indexable": false,
         "color": "28b62c"
@@ -488,9 +488,9 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "4200ad41-d032-476a-acb4-943e6481ee6d",
+    "pk": "60e8c1f1-c04d-49b6-bc6e-70e38e437f36",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
         "slug": "volume",
         "display_name": "Volume",
         "folder": true,
@@ -500,11 +500,11 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "634e6aee-87a1-4845-a63e-83e7226aa557",
+    "pk": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "text_line",
-        "display_name": "Line",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "slug": "word",
+        "display_name": "Word",
         "folder": false,
         "indexable": false,
         "color": "28b62c"
@@ -512,11 +512,11 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "7afe34f9-ad57-4ce7-872a-4d9aa31b6d0b",
+    "pk": "c6267733-b1ce-4ea3-ac20-3a18c3d66f24",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "page",
-        "display_name": "Page",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "slug": "act",
+        "display_name": "Act",
         "folder": false,
         "indexable": false,
         "color": "28b62c"
@@ -524,11 +524,11 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "bc871e02-a23f-4799-a4d7-bb81813f41f5",
+    "pk": "f2d75650-9d48-4b6e-b210-dbd595a668d2",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "surface",
-        "display_name": "Surface",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "slug": "page",
+        "display_name": "Page",
         "folder": false,
         "indexable": false,
         "color": "28b62c"
@@ -536,11 +536,11 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "eda3da9f-1a6b-4f7a-91d1-f28e4e74f76a",
+    "pk": "fb9cf6d3-b31a-449c-a780-2f5db501695f",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "act",
-        "display_name": "Act",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "slug": "surface",
+        "display_name": "Surface",
         "folder": false,
         "indexable": false,
         "color": "28b62c"
@@ -548,279 +548,279 @@
 },
 {
     "model": "documents.elementpath",
-    "pk": "04489822-c85b-4ac8-8d59-f7e1ac914295",
-    "fields": {
-        "element": "6c6a774e-88c3-40c5-9913-d7ec2a3d5125",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 0
-    }
-},
-{
-    "model": "documents.elementpath",
-    "pk": "0a801b84-7243-4ee0-857a-446a6af3ae97",
+    "pk": "13930713-e133-4d00-90fe-7169541b6b8c",
     "fields": {
-        "element": "3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64",
-        "path": "[]",
+        "element": "1f20a3a4-33be-43a3-9427-393799aba6d7",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"737db4ff-8119-4e12-be99-968d7fa3be22\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "0aad9073-0d49-492e-8455-4bd99e492e89",
+    "pk": "250c8b60-16ac-49c9-ba2a-ee157d387077",
     "fields": {
-        "element": "469f4606-f5ea-406d-a0bd-6d48050c39ed",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"4d2f59fe-9808-44b1-b3b5-017490ae4064\"]",
-        "ordering": 0
+        "element": "60a6c062-1bbb-4e5f-82e7-37cb77d92419",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"67482d70-765b-4092-b462-2d955fd8a7df\"]",
+        "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "2a801688-b438-4234-a1de-4b06553f1d55",
+    "pk": "2d34a587-68b2-4087-a05e-d92c5bc0b086",
     "fields": {
-        "element": "d3dc3f63-9894-434b-9715-7bd906b63e63",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6c6a774e-88c3-40c5-9913-d7ec2a3d5125\"]",
-        "ordering": 0
+        "element": "6452b6c6-3d58-48c2-9032-691095fe8317",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"67482d70-765b-4092-b462-2d955fd8a7df\"]",
+        "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "394d7eca-1ccc-47a4-ae44-8dd33c9ceb17",
+    "pk": "33cdaa32-f9fe-4d60-91af-6200f90915af",
     "fields": {
-        "element": "60313262-3273-4c39-a8ee-cedc733f6990",
-        "path": "[\"7a007e29-b509-4c4e-8321-78464df6f03a\"]",
-        "ordering": 0
+        "element": "737db4ff-8119-4e12-be99-968d7fa3be22",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\"]",
+        "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "3d71eb28-24c5-4028-a72b-a3c170c117eb",
+    "pk": "344c8906-d207-48cc-9d5b-5c738758e510",
     "fields": {
-        "element": "17a65610-023b-46c7-93a6-453335626d17",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6c6a774e-88c3-40c5-9913-d7ec2a3d5125\"]",
-        "ordering": 3
+        "element": "66843ee4-559b-4d2d-a9ce-4dd425022159",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"737db4ff-8119-4e12-be99-968d7fa3be22\"]",
+        "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "4cd70fa4-1e6e-452a-8212-4be6138a6b34",
+    "pk": "36612194-8586-43a3-9114-b3fa5d4c7832",
     "fields": {
-        "element": "c16df32e-03b5-47e4-868f-98d57eac1f15",
-        "path": "[\"7a007e29-b509-4c4e-8321-78464df6f03a\"]",
-        "ordering": 1
+        "element": "8349d4ba-fd23-4da6-a99b-76ba2e359b6f",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\"]",
+        "ordering": 5
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "4e407215-0ed2-4ff2-98ff-a22d6b23349a",
+    "pk": "37f898d5-0a9a-49df-b8f6-40761f18c734",
     "fields": {
-        "element": "a8c108a2-a6b7-4401-9d64-65cf263531a7",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6c6a774e-88c3-40c5-9913-d7ec2a3d5125\"]",
+        "element": "a41e2061-65cf-4c6e-9ea3-48d00d74133d",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"a0373c38-907d-4127-9762-5aada56b7165\"]",
         "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "56655c48-69b4-4ea0-a724-b10d1a47e231",
+    "pk": "4450a6b1-88fb-4d11-9c7a-edd03872050d",
     "fields": {
-        "element": "b8c4d1e3-aa9d-425a-b70f-c8ff16ae147c",
-        "path": "[\"7a007e29-b509-4c4e-8321-78464df6f03a\"]",
-        "ordering": 2
+        "element": "7f80cd7d-f24f-4691-a139-0ce744ea7577",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"492ea0b0-bcaf-4a46-a15a-29cd08e32d90\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "5c064e36-d3b5-4a8b-8f2d-97da74e84a70",
+    "pk": "48d847e6-26be-45e8-92a4-0a6517227172",
     "fields": {
-        "element": "86f68e65-f23b-4d01-a9ca-c40eed9cda32",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 6
+        "element": "8727b32e-6b08-44fd-84ff-54d505fb5a85",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"a0373c38-907d-4127-9762-5aada56b7165\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "617f3ea6-17f8-4514-9f87-4c8bdd747e3f",
+    "pk": "5bb0b100-e35c-4433-884e-16b3c827dea1",
     "fields": {
-        "element": "4d2f59fe-9808-44b1-b3b5-017490ae4064",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 2
+        "element": "e2e0ab90-85c8-457e-920d-479bebb43bf9",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"67482d70-765b-4092-b462-2d955fd8a7df\"]",
+        "ordering": 3
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "6cc0a005-dbff-4b53-b452-b6d4d8ec8eb3",
+    "pk": "60fccadc-2668-45f0-af63-8541f9d8000a",
     "fields": {
-        "element": "09ee64d6-3d8d-4d74-a1cd-ec34c8c2cd38",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"664f19d6-472d-49cc-bd87-26c19db7d2d3\"]",
+        "element": "900dcb10-2146-4571-8735-f508eb5f9986",
+        "path": "[]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "8adb5d49-8651-4239-b98d-425a70ff39cf",
+    "pk": "6b954042-1718-4794-aad4-55c6e2e9e054",
     "fields": {
-        "element": "6a41ddf7-41e4-40ad-8234-d773b74058bc",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 1
+        "element": "7cd8a970-ca3a-479f-b49e-1292b216c1b9",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"492ea0b0-bcaf-4a46-a15a-29cd08e32d90\"]",
+        "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a2d6b2bd-677e-4f72-9a9d-64957b854a90",
+    "pk": "6ff93429-322f-486a-bc0f-31f7047ce341",
     "fields": {
-        "element": "18ada82e-36c6-47e4-bcff-bee5aa63889b",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"4d2f59fe-9808-44b1-b3b5-017490ae4064\"]",
+        "element": "c80311e3-4529-45d6-9575-31b362c99925",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"737db4ff-8119-4e12-be99-968d7fa3be22\"]",
         "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a3d290d2-c6a1-4c80-8baf-37d42497e61e",
+    "pk": "8fd6c9e9-3133-4733-b58b-8a093e49288c",
     "fields": {
-        "element": "d227268a-2c2e-40ff-b22c-ab6e52eb5bba",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 3
+        "element": "f6b8eced-ee41-42d3-812a-a622f1879809",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"8144a0f8-f75b-43fa-ac27-d49d69bc483c\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a735c855-bd4b-4ece-8345-eac1dc791ff9",
+    "pk": "98f8328f-77b3-490d-aa4c-97d6f2030e3f",
     "fields": {
-        "element": "ba888ed5-8c85-4f49-8f87-499219187ade",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 5
+        "element": "dc79378e-1804-45c8-b23f-a2eeb33734de",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"8349d4ba-fd23-4da6-a99b-76ba2e359b6f\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a8012a46-1e99-40a4-ad86-d2b2040eb026",
+    "pk": "9979cf91-e6da-46e4-976e-386cb9ebbc42",
     "fields": {
-        "element": "4c054d5e-f30c-4f70-a368-8e6f78f48ba1",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"d227268a-2c2e-40ff-b22c-ab6e52eb5bba\"]",
-        "ordering": 0
+        "element": "7ac6504c-4088-4e6e-9a48-567f1f7e54c8",
+        "path": "[\"900dcb10-2146-4571-8735-f508eb5f9986\"]",
+        "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a8b4a820-4de5-4164-88e9-66b3fd875938",
+    "pk": "99ac36ba-7c91-4beb-a10b-97d8edffa1d9",
     "fields": {
-        "element": "34cdee1d-40e3-4499-b04b-93f6d2bfb802",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6c6a774e-88c3-40c5-9913-d7ec2a3d5125\"]",
-        "ordering": 2
+        "element": "12e84ea0-004a-43e7-9fcc-85021e2407a9",
+        "path": "[]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "ab00de7e-015c-4f76-a3c0-645cb879a024",
+    "pk": "9feb4874-46bf-463a-9bc9-71b13af8bcc0",
     "fields": {
-        "element": "2c0f28b1-b8e9-4f08-9014-75ce7950ee6f",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6a41ddf7-41e4-40ad-8234-d773b74058bc\"]",
+        "element": "e38ab791-092f-48eb-bb09-ea95e5a01998",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"492ea0b0-bcaf-4a46-a15a-29cd08e32d90\"]",
         "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "b100fd6e-d7ed-4481-8604-f07e9d554637",
+    "pk": "b447f917-b73c-42b5-9512-1a3af17b3eee",
     "fields": {
-        "element": "3603393d-4831-4dbf-9672-3fbe1077e03a",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"4d2f59fe-9808-44b1-b3b5-017490ae4064\"]",
-        "ordering": 1
+        "element": "a0373c38-907d-4127-9762-5aada56b7165",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\"]",
+        "ordering": 4
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "b7098cdc-dcba-4efc-8213-e92af55fa6f9",
+    "pk": "b869ccff-bb9b-4c85-9743-d8930f2d920a",
     "fields": {
-        "element": "3f3bb3ee-ec7c-436c-9dde-f8aa0e1a69d7",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
+        "element": "f8d56f23-51f6-40dd-9482-939a1dd877b3",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\"]",
         "ordering": 7
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "b992817b-343e-4a5b-bf6d-42cb334696d1",
+    "pk": "b8a8076c-62d4-4f86-a2f0-21187d589279",
     "fields": {
-        "element": "b0a0984b-dcf2-46da-8d6e-933020a46011",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6a41ddf7-41e4-40ad-8234-d773b74058bc\"]",
-        "ordering": 2
+        "element": "8144a0f8-f75b-43fa-ac27-d49d69bc483c",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\"]",
+        "ordering": 6
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "c35f1e16-f92d-4589-afc4-6c3115fa36d7",
+    "pk": "be089329-1d86-42cb-85b6-3a2520a334ed",
     "fields": {
-        "element": "9fd626f8-a5eb-4ae3-9f4f-f0ea4216712e",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"664f19d6-472d-49cc-bd87-26c19db7d2d3\"]",
+        "element": "492ea0b0-bcaf-4a46-a15a-29cd08e32d90",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\"]",
         "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "d0586c21-e60a-41d8-9a17-b71eeb75bc68",
+    "pk": "c66e5e45-f39b-42b7-bbf7-55bab78bfe2c",
     "fields": {
-        "element": "e2f8eee0-cf55-404c-86e6-6950ab88faac",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"3f3bb3ee-ec7c-436c-9dde-f8aa0e1a69d7\"]",
+        "element": "72dd24ed-22f5-40a6-bbb4-b4b0abfcd758",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"f8d56f23-51f6-40dd-9482-939a1dd877b3\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "da4fe1fb-7ad7-4b38-973f-c9a4ca8d402f",
+    "pk": "cc4ca9d4-57ac-4b86-aa08-848fb31c15f1",
+    "fields": {
+        "element": "43ba23ed-3e59-466d-b48f-35928d3d9a59",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\"]",
+        "ordering": 3
+    }
+},
+{
+    "model": "documents.elementpath",
+    "pk": "d28feecf-d0a3-4bd6-b045-71cae47ccf4f",
     "fields": {
-        "element": "3189dc31-fa75-4c60-b1d0-7a15720e35ca",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"86f68e65-f23b-4d01-a9ca-c40eed9cda32\"]",
+        "element": "67482d70-765b-4092-b462-2d955fd8a7df",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "e2c73d2f-8881-4b4e-909f-7244abd4c92c",
+    "pk": "d840ea1c-695a-4e81-bfb1-ccc2c7746979",
     "fields": {
-        "element": "664f19d6-472d-49cc-bd87-26c19db7d2d3",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 4
+        "element": "f09bebdb-152e-427c-ae2b-2e296982f342",
+        "path": "[\"900dcb10-2146-4571-8735-f508eb5f9986\"]",
+        "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "e6c19e34-49a7-4dc2-aa37-8d96f2a7b43e",
+    "pk": "dd98d35b-a3dd-48fa-bec0-1e932f9314ff",
     "fields": {
-        "element": "7a007e29-b509-4c4e-8321-78464df6f03a",
-        "path": "[]",
+        "element": "2aaa4788-127e-464c-bd8c-cd938796d207",
+        "path": "[\"900dcb10-2146-4571-8735-f508eb5f9986\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "e9d64de1-c0c6-44a1-bb33-38c87a144282",
+    "pk": "de39b960-87d6-4805-bd39-93debf36f7b3",
     "fields": {
-        "element": "d5daac1b-5f57-4d3c-b120-54adbcc82d7d",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"ba888ed5-8c85-4f49-8f87-499219187ade\"]",
+        "element": "ba400ee3-7247-4c20-bef4-3a49ad9e2e77",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"67482d70-765b-4092-b462-2d955fd8a7df\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "ed3283f8-0af1-467e-8a52-77f9d434af78",
+    "pk": "f0042f9c-66a9-448d-a202-03b55d40d03d",
     "fields": {
-        "element": "92101c2b-885f-42e2-a0cb-13a8a04c0ea8",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6a41ddf7-41e4-40ad-8234-d773b74058bc\"]",
+        "element": "9f1c4e10-d94c-4628-a575-5cb0afdd34af",
+        "path": "[\"12e84ea0-004a-43e7-9fcc-85021e2407a9\", \"43ba23ed-3e59-466d-b48f-35928d3d9a59\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.element",
-    "pk": "09ee64d6-3d8d-4d74-a1cd-ec34c8c2cd38",
+    "pk": "12e84ea0-004a-43e7-9fcc-85021e2407a9",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "bc871e02-a23f-4799-a4d7-bb81813f41f5",
-        "name": "Surface B",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "60e8c1f1-c04d-49b6-bc6e-70e38e437f36",
+        "name": "Volume 1",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
-        "polygon": "LINEARRING (600 600, 600 1000, 1000 1000, 1000 600, 600 600)",
+        "image": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -828,18 +828,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "17a65610-023b-46c7-93a6-453335626d17",
+    "pk": "1f20a3a4-33be-43a3-9427-393799aba6d7",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "634e6aee-87a1-4845-a63e-83e7226aa557",
-        "name": "Text line",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
+        "name": "PARIS",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
-        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
+        "image": "68df44c1-ed1f-4f29-870d-de7b4b03b1a5",
+        "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -847,18 +847,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "18ada82e-36c6-47e4-bcff-bee5aa63889b",
+    "pk": "2aaa4788-127e-464c-bd8c-cd938796d207",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
-        "name": "DATUM",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "f2d75650-9d48-4b6e-b210-dbd595a668d2",
+        "name": "Volume 2, page 1r",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
-        "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
+        "image": "a68c21d7-d24f-44d7-b134-3c50ee6ddba1",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -866,18 +866,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "2c0f28b1-b8e9-4f08-9014-75ce7950ee6f",
+    "pk": "43ba23ed-3e59-466d-b48f-35928d3d9a59",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
-        "name": "ROY",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "c6267733-b1ce-4ea3-ac20-3a18c3d66f24",
+        "name": "Act 1",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "47f164d3-d26f-41e8-9df1-b538645d60b5",
-        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
+        "image": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -885,18 +885,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "3189dc31-fa75-4c60-b1d0-7a15720e35ca",
+    "pk": "492ea0b0-bcaf-4a46-a15a-29cd08e32d90",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "bc871e02-a23f-4799-a4d7-bb81813f41f5",
-        "name": "Surface E",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "f2d75650-9d48-4b6e-b210-dbd595a668d2",
+        "name": "Volume 1, page 1v",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
-        "polygon": "LINEARRING (300 300, 300 600, 600 600, 600 300, 300 300)",
+        "image": "7f006318-b3d2-4cda-b1ab-8ff844c07b35",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -904,17 +904,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "34cdee1d-40e3-4499-b04b-93f6d2bfb802",
+    "pk": "60a6c062-1bbb-4e5f-82e7-37cb77d92419",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
         "name": "DATUM",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
+        "image": "627a53a6-466c-4114-b826-852ecd328e47",
         "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -923,17 +923,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "3603393d-4831-4dbf-9672-3fbe1077e03a",
+    "pk": "6452b6c6-3d58-48c2-9032-691095fe8317",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
         "name": "ROY",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
+        "image": "627a53a6-466c-4114-b826-852ecd328e47",
         "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -942,18 +942,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64",
+    "pk": "66843ee4-559b-4d2d-a9ce-4dd425022159",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "4200ad41-d032-476a-acb4-943e6481ee6d",
-        "name": "Volume 1",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
+        "name": "ROY",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "68df44c1-ed1f-4f29-870d-de7b4b03b1a5",
+        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -961,18 +961,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "3f3bb3ee-ec7c-436c-9dde-f8aa0e1a69d7",
+    "pk": "67482d70-765b-4092-b462-2d955fd8a7df",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "eda3da9f-1a6b-4f7a-91d1-f28e4e74f76a",
-        "name": "Act 5",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "f2d75650-9d48-4b6e-b210-dbd595a668d2",
+        "name": "Volume 1, page 1r",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "627a53a6-466c-4114-b826-852ecd328e47",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -980,18 +980,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "469f4606-f5ea-406d-a0bd-6d48050c39ed",
+    "pk": "72dd24ed-22f5-40a6-bbb4-b4b0abfcd758",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
-        "name": "PARIS",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "fb9cf6d3-b31a-449c-a780-2f5db501695f",
+        "name": "Surface F",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
-        "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
+        "image": "68df44c1-ed1f-4f29-870d-de7b4b03b1a5",
+        "polygon": "LINEARRING (600 600, 600 1000, 1000 1000, 1000 600, 600 600)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -999,18 +999,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "4c054d5e-f30c-4f70-a368-8e6f78f48ba1",
+    "pk": "737db4ff-8119-4e12-be99-968d7fa3be22",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "bc871e02-a23f-4799-a4d7-bb81813f41f5",
-        "name": "Surface A",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "f2d75650-9d48-4b6e-b210-dbd595a668d2",
+        "name": "Volume 1, page 2r",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
-        "polygon": "LINEARRING (0 0, 0 600, 600 600, 600 0, 0 0)",
+        "image": "68df44c1-ed1f-4f29-870d-de7b4b03b1a5",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1018,17 +1018,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "4d2f59fe-9808-44b1-b3b5-017490ae4064",
+    "pk": "7ac6504c-4088-4e6e-9a48-567f1f7e54c8",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "7afe34f9-ad57-4ce7-872a-4d9aa31b6d0b",
-        "name": "Volume 1, page 2r",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "f2d75650-9d48-4b6e-b210-dbd595a668d2",
+        "name": "Volume 2, page 1v",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
+        "image": "4867c2a1-fadc-4086-842c-c914b678d37d",
         "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -1037,18 +1037,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "60313262-3273-4c39-a8ee-cedc733f6990",
+    "pk": "7cd8a970-ca3a-479f-b49e-1292b216c1b9",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "7afe34f9-ad57-4ce7-872a-4d9aa31b6d0b",
-        "name": "Volume 2, page 1r",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
+        "name": "DATUM",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "2552f45d-6880-4a4c-b9c2-1091598a52b4",
-        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
+        "image": "7f006318-b3d2-4cda-b1ab-8ff844c07b35",
+        "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1056,13 +1056,32 @@
 },
 {
     "model": "documents.element",
-    "pk": "664f19d6-472d-49cc-bd87-26c19db7d2d3",
+    "pk": "7f80cd7d-f24f-4691-a139-0ce744ea7577",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "eda3da9f-1a6b-4f7a-91d1-f28e4e74f76a",
-        "name": "Act 2",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
+        "name": "PARIS",
+        "creator": null,
+        "worker_version": null,
+        "worker_run": null,
+        "image": "7f006318-b3d2-4cda-b1ab-8ff844c07b35",
+        "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
+        "rotation_angle": 0,
+        "mirrored": false,
+        "confidence": null
+    }
+},
+{
+    "model": "documents.element",
+    "pk": "8144a0f8-f75b-43fa-ac27-d49d69bc483c",
+    "fields": {
+        "created": "2020-02-02T01:23:45.678Z",
+        "updated": "2020-02-02T01:23:45.678Z",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "c6267733-b1ce-4ea3-ac20-3a18c3d66f24",
+        "name": "Act 4",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
@@ -1075,18 +1094,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "6a41ddf7-41e4-40ad-8234-d773b74058bc",
+    "pk": "8349d4ba-fd23-4da6-a99b-76ba2e359b6f",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "7afe34f9-ad57-4ce7-872a-4d9aa31b6d0b",
-        "name": "Volume 1, page 1v",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "c6267733-b1ce-4ea3-ac20-3a18c3d66f24",
+        "name": "Act 3",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "47f164d3-d26f-41e8-9df1-b538645d60b5",
-        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
+        "image": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1094,18 +1113,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "6c6a774e-88c3-40c5-9913-d7ec2a3d5125",
+    "pk": "8727b32e-6b08-44fd-84ff-54d505fb5a85",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "7afe34f9-ad57-4ce7-872a-4d9aa31b6d0b",
-        "name": "Volume 1, page 1r",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "fb9cf6d3-b31a-449c-a780-2f5db501695f",
+        "name": "Surface B",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
-        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
+        "image": "627a53a6-466c-4114-b826-852ecd328e47",
+        "polygon": "LINEARRING (600 600, 600 1000, 1000 1000, 1000 600, 600 600)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1113,12 +1132,12 @@
 },
 {
     "model": "documents.element",
-    "pk": "7a007e29-b509-4c4e-8321-78464df6f03a",
+    "pk": "900dcb10-2146-4571-8735-f508eb5f9986",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "4200ad41-d032-476a-acb4-943e6481ee6d",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "60e8c1f1-c04d-49b6-bc6e-70e38e437f36",
         "name": "Volume 2",
         "creator": null,
         "worker_version": null,
@@ -1132,18 +1151,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "86f68e65-f23b-4d01-a9ca-c40eed9cda32",
+    "pk": "9f1c4e10-d94c-4628-a575-5cb0afdd34af",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "eda3da9f-1a6b-4f7a-91d1-f28e4e74f76a",
-        "name": "Act 4",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "fb9cf6d3-b31a-449c-a780-2f5db501695f",
+        "name": "Surface A",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "627a53a6-466c-4114-b826-852ecd328e47",
+        "polygon": "LINEARRING (0 0, 0 600, 600 600, 600 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1151,18 +1170,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "92101c2b-885f-42e2-a0cb-13a8a04c0ea8",
+    "pk": "a0373c38-907d-4127-9762-5aada56b7165",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
-        "name": "PARIS",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "c6267733-b1ce-4ea3-ac20-3a18c3d66f24",
+        "name": "Act 2",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "47f164d3-d26f-41e8-9df1-b538645d60b5",
-        "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
+        "image": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1170,17 +1189,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "9fd626f8-a5eb-4ae3-9f4f-f0ea4216712e",
+    "pk": "a41e2061-65cf-4c6e-9ea3-48d00d74133d",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "bc871e02-a23f-4799-a4d7-bb81813f41f5",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "fb9cf6d3-b31a-449c-a780-2f5db501695f",
         "name": "Surface C",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "47f164d3-d26f-41e8-9df1-b538645d60b5",
+        "image": "7f006318-b3d2-4cda-b1ab-8ff844c07b35",
         "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -1189,18 +1208,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "a8c108a2-a6b7-4401-9d64-65cf263531a7",
+    "pk": "ba400ee3-7247-4c20-bef4-3a49ad9e2e77",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
-        "name": "ROY",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
+        "name": "PARIS",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
-        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
+        "image": "627a53a6-466c-4114-b826-852ecd328e47",
+        "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1208,17 +1227,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "b0a0984b-dcf2-46da-8d6e-933020a46011",
+    "pk": "c80311e3-4529-45d6-9575-31b362c99925",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
         "name": "DATUM",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "47f164d3-d26f-41e8-9df1-b538645d60b5",
+        "image": "68df44c1-ed1f-4f29-870d-de7b4b03b1a5",
         "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -1227,18 +1246,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "b8c4d1e3-aa9d-425a-b70f-c8ff16ae147c",
+    "pk": "dc79378e-1804-45c8-b23f-a2eeb33734de",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "7afe34f9-ad57-4ce7-872a-4d9aa31b6d0b",
-        "name": "Volume 2, page 2r",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "fb9cf6d3-b31a-449c-a780-2f5db501695f",
+        "name": "Surface D",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "4b93eabe-b42d-4b88-8f31-a63d0258000a",
-        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
+        "image": "68df44c1-ed1f-4f29-870d-de7b4b03b1a5",
+        "polygon": "LINEARRING (0 0, 0 300, 300 300, 300 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1246,18 +1265,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "ba888ed5-8c85-4f49-8f87-499219187ade",
+    "pk": "e2e0ab90-85c8-457e-920d-479bebb43bf9",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "eda3da9f-1a6b-4f7a-91d1-f28e4e74f76a",
-        "name": "Act 3",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "097f73b9-4ea7-4063-98b0-e17eb698e77a",
+        "name": "Text line",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "627a53a6-466c-4114-b826-852ecd328e47",
+        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1265,18 +1284,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "c16df32e-03b5-47e4-868f-98d57eac1f15",
+    "pk": "e38ab791-092f-48eb-bb09-ea95e5a01998",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "7afe34f9-ad57-4ce7-872a-4d9aa31b6d0b",
-        "name": "Volume 2, page 1v",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "aa7b5e5a-eaa3-49b5-8c7a-826941c9d472",
+        "name": "ROY",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "5de01fda-de62-4d26-b8bf-5c3f4678de3b",
-        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
+        "image": "7f006318-b3d2-4cda-b1ab-8ff844c07b35",
+        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1284,37 +1303,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "d227268a-2c2e-40ff-b22c-ab6e52eb5bba",
+    "pk": "f09bebdb-152e-427c-ae2b-2e296982f342",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "eda3da9f-1a6b-4f7a-91d1-f28e4e74f76a",
-        "name": "Act 1",
-        "creator": null,
-        "worker_version": null,
-        "worker_run": null,
-        "image": null,
-        "polygon": null,
-        "rotation_angle": 0,
-        "mirrored": false,
-        "confidence": null
-    }
-},
-{
-    "model": "documents.element",
-    "pk": "d3dc3f63-9894-434b-9715-7bd906b63e63",
-    "fields": {
-        "created": "2020-02-02T01:23:45.678Z",
-        "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "3bbbd33a-b906-4526-9e0b-0643ade1b080",
-        "name": "PARIS",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "f2d75650-9d48-4b6e-b210-dbd595a668d2",
+        "name": "Volume 2, page 2r",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
-        "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
+        "image": "d7f4c84a-8aae-4c7d-86ce-567c5d4adc6d",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1322,18 +1322,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "d5daac1b-5f57-4d3c-b120-54adbcc82d7d",
+    "pk": "f6b8eced-ee41-42d3-812a-a622f1879809",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "bc871e02-a23f-4799-a4d7-bb81813f41f5",
-        "name": "Surface D",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "fb9cf6d3-b31a-449c-a780-2f5db501695f",
+        "name": "Surface E",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
-        "polygon": "LINEARRING (0 0, 0 300, 300 300, 300 0, 0 0)",
+        "image": "68df44c1-ed1f-4f29-870d-de7b4b03b1a5",
+        "polygon": "LINEARRING (300 300, 300 600, 600 600, 600 300, 300 300)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1341,18 +1341,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "e2f8eee0-cf55-404c-86e6-6950ab88faac",
+    "pk": "f8d56f23-51f6-40dd-9482-939a1dd877b3",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "bc871e02-a23f-4799-a4d7-bb81813f41f5",
-        "name": "Surface F",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "c6267733-b1ce-4ea3-ac20-3a18c3d66f24",
+        "name": "Act 5",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
-        "polygon": "LINEARRING (600 600, 600 1000, 1000 1000, 1000 600, 600 600)",
+        "image": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1360,91 +1360,91 @@
 },
 {
     "model": "documents.entitytype",
-    "pk": "4e1138f1-be4c-4205-834b-b2795e2bd1a3",
+    "pk": "4e9c9ca3-03ae-4994-b617-4d78f0854dc0",
     "fields": {
-        "name": "date",
+        "name": "person",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337"
     }
 },
 {
     "model": "documents.entitytype",
-    "pk": "564768cc-38be-45be-9e61-8dcade951c00",
+    "pk": "8c7ad642-50eb-4125-899b-aa3d03024a47",
     "fields": {
         "name": "location",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337"
     }
 },
 {
     "model": "documents.entitytype",
-    "pk": "a773549d-4adf-4dde-a595-3b2318f5cc0d",
+    "pk": "95fcaaf8-6a17-4540-8a76-866a6cad9f1d",
     "fields": {
-        "name": "person",
+        "name": "number",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337"
     }
 },
 {
     "model": "documents.entitytype",
-    "pk": "e19eb34b-55eb-4a8f-8019-18eea8e62fba",
+    "pk": "974f88d0-2e4c-4a9e-85b7-2e244c2e883f",
     "fields": {
-        "name": "number",
+        "name": "date",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337"
     }
 },
 {
     "model": "documents.entitytype",
-    "pk": "e96f8757-37d7-4457-9f2a-8e870cdadae5",
+    "pk": "e82c02b6-9b63-483a-a3c0-94fe032d83f8",
     "fields": {
         "name": "organization",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337"
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "28e2570a-4022-4718-8827-5be712a53d4b",
+    "pk": "268de903-300e-4a00-8f10-e2e10ac2a733",
     "fields": {
-        "element": "3603393d-4831-4dbf-9672-3fbe1077e03a",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "60a6c062-1bbb-4e5f-82e7-37cb77d92419",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "ROY",
+        "text": "DATUM",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "626eaeaf-ae24-4bc0-b1f6-78ad744a3645",
+    "pk": "53d8a708-cd44-40b8-8862-7f663ece1133",
     "fields": {
-        "element": "6c6a774e-88c3-40c5-9913-d7ec2a3d5125",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "e38ab791-092f-48eb-bb09-ea95e5a01998",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "Lorem ipsum dolor sit amet",
+        "text": "ROY",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "62b7b9e6-0869-444c-aa8b-054fc124f6d9",
+    "pk": "56a95e2d-4262-42da-a435-a5f29c0bc094",
     "fields": {
-        "element": "a8c108a2-a6b7-4401-9d64-65cf263531a7",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "ba400ee3-7247-4c20-bef4-3a49ad9e2e77",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "ROY",
+        "text": "PARIS",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "6f477447-1b65-44f6-a912-6ff6b24165a2",
+    "pk": "7591f452-3d64-4b43-9789-0f05f2dfd6f8",
     "fields": {
-        "element": "2c0f28b1-b8e9-4f08-9014-75ce7950ee6f",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "6452b6c6-3d58-48c2-9032-691095fe8317",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
         "text": "ROY",
         "orientation": "horizontal-lr",
@@ -1453,111 +1453,111 @@
 },
 {
     "model": "documents.transcription",
-    "pk": "797bde96-8b76-4a9b-a4b9-b3378fcfdcdf",
+    "pk": "9b348778-35b7-4ed8-8232-4a641d51118d",
     "fields": {
-        "element": "18ada82e-36c6-47e4-bcff-bee5aa63889b",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "7f80cd7d-f24f-4691-a139-0ce744ea7577",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "DATUM",
+        "text": "PARIS",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "7b0f48b5-0580-47ef-9927-5e5fbdb749ff",
+    "pk": "dd193fa4-103c-43ae-8914-ed24f2487251",
     "fields": {
-        "element": "469f4606-f5ea-406d-a0bd-6d48050c39ed",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "7cd8a970-ca3a-479f-b49e-1292b216c1b9",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "PARIS",
+        "text": "DATUM",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "a94dabcd-9e37-4cbc-a45a-6e9a5a76b01a",
+    "pk": "de4d3e9b-597b-483a-8178-0c78328655bf",
     "fields": {
-        "element": "d3dc3f63-9894-434b-9715-7bd906b63e63",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "66843ee4-559b-4d2d-a9ce-4dd425022159",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "PARIS",
+        "text": "ROY",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "e74d7029-86e6-41ad-beed-12f4b215fb57",
+    "pk": "e37e143e-c6b5-4b3f-82ce-37e60fd188c1",
     "fields": {
-        "element": "34cdee1d-40e3-4499-b04b-93f6d2bfb802",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "1f20a3a4-33be-43a3-9427-393799aba6d7",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "DATUM",
+        "text": "PARIS",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "eae51f9a-eb40-4d27-946f-274231062d30",
+    "pk": "e41a7db5-5c44-4a14-88e9-92d7769263ca",
     "fields": {
-        "element": "92101c2b-885f-42e2-a0cb-13a8a04c0ea8",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "c80311e3-4529-45d6-9575-31b362c99925",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "PARIS",
+        "text": "DATUM",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "fb8d4786-a627-4049-9433-20755e7f3ca5",
+    "pk": "fe88b0e9-117e-4c9b-a761-a8e63f2f095a",
     "fields": {
-        "element": "b0a0984b-dcf2-46da-8d6e-933020a46011",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "67482d70-765b-4092-b462-2d955fd8a7df",
+        "worker_version": "06e2171f-b7d6-49dc-90fc-d47784e6e334",
         "worker_run": null,
-        "text": "DATUM",
+        "text": "Lorem ipsum dolor sit amet",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.allowedmetadata",
-    "pk": "7c15d98e-10f2-4c0b-809f-b65242d5e74b",
+    "pk": "13e9fa71-94a9-4e47-95a4-291597a811ef",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "date",
-        "name": "date"
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "text",
+        "name": "folio"
     }
 },
 {
     "model": "documents.allowedmetadata",
-    "pk": "819968c2-2433-4597-ac8b-852ed3397a9a",
+    "pk": "721a146d-a883-4f73-981a-1d90744d6d13",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "location",
-        "name": "location"
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "date",
+        "name": "date"
     }
 },
 {
     "model": "documents.allowedmetadata",
-    "pk": "c079f5d7-f039-43a5-93c6-9019e8203e6b",
+    "pk": "7b693888-219e-4f24-a930-de812973dc27",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "text",
-        "name": "folio"
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
+        "type": "location",
+        "name": "location"
     }
 },
 {
     "model": "documents.metadata",
-    "pk": "03221610-3351-472d-9f7e-b23d7cbc7b92",
+    "pk": "0d46167f-853f-4a07-b780-216a8c400aa3",
     "fields": {
-        "element": "6a41ddf7-41e4-40ad-8234-d773b74058bc",
-        "name": "folio",
+        "element": "8349d4ba-fd23-4da6-a99b-76ba2e359b6f",
+        "name": "number",
         "type": "text",
-        "value": "1v",
+        "value": "3",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1565,12 +1565,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "18459ddf-615f-47cc-8de6-11ab62446632",
+    "pk": "2cafc35d-ea3c-48cb-a93c-907d6f0a8643",
     "fields": {
-        "element": "664f19d6-472d-49cc-bd87-26c19db7d2d3",
-        "name": "number",
+        "element": "2aaa4788-127e-464c-bd8c-cd938796d207",
+        "name": "folio",
         "type": "text",
-        "value": "2",
+        "value": "1r",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1578,12 +1578,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "34ec1306-1d88-4fc4-8049-d157252c5d51",
+    "pk": "435e31bc-7844-474b-aff4-c3b8fcc6927a",
     "fields": {
-        "element": "6c6a774e-88c3-40c5-9913-d7ec2a3d5125",
-        "name": "folio",
+        "element": "43ba23ed-3e59-466d-b48f-35928d3d9a59",
+        "name": "number",
         "type": "text",
-        "value": "1r",
+        "value": "1",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1591,12 +1591,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "5712f834-778d-450b-ba46-86262c4a13f8",
+    "pk": "44548048-3180-4912-9134-10ae87037392",
     "fields": {
-        "element": "ba888ed5-8c85-4f49-8f87-499219187ade",
-        "name": "number",
+        "element": "737db4ff-8119-4e12-be99-968d7fa3be22",
+        "name": "folio",
         "type": "text",
-        "value": "3",
+        "value": "2r",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1604,9 +1604,9 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "7d8d0dc1-fcf6-4e47-9b03-25d5baaac590",
+    "pk": "57fb874a-5aa8-4526-9cf8-ad5c10b38d24",
     "fields": {
-        "element": "86f68e65-f23b-4d01-a9ca-c40eed9cda32",
+        "element": "8144a0f8-f75b-43fa-ac27-d49d69bc483c",
         "name": "number",
         "type": "text",
         "value": "4",
@@ -1617,12 +1617,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "820d9d2f-c6d0-40df-9bce-cb9062f17b0b",
+    "pk": "9ea4ce55-d95c-46d2-802e-89f98abe862b",
     "fields": {
-        "element": "60313262-3273-4c39-a8ee-cedc733f6990",
+        "element": "f09bebdb-152e-427c-ae2b-2e296982f342",
         "name": "folio",
         "type": "text",
-        "value": "1r",
+        "value": "2r",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1630,12 +1630,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "93e2a919-9320-47ce-9b0f-de178dab40bc",
+    "pk": "b22d936a-386d-48e3-88e0-6b88da42c982",
     "fields": {
-        "element": "3f3bb3ee-ec7c-436c-9dde-f8aa0e1a69d7",
-        "name": "number",
+        "element": "492ea0b0-bcaf-4a46-a15a-29cd08e32d90",
+        "name": "folio",
         "type": "text",
-        "value": "5",
+        "value": "1v",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1643,12 +1643,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "9fc32596-bd62-4f8d-894d-abfeefc6157a",
+    "pk": "d58788f8-58a9-488a-bef9-0302705ff942",
     "fields": {
-        "element": "4d2f59fe-9808-44b1-b3b5-017490ae4064",
-        "name": "folio",
+        "element": "f8d56f23-51f6-40dd-9482-939a1dd877b3",
+        "name": "number",
         "type": "text",
-        "value": "2r",
+        "value": "5",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1656,12 +1656,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "ba9916fa-52e6-4203-8db4-662f9083c70c",
+    "pk": "de810d60-9a4b-42e3-85c4-7a2174a01213",
     "fields": {
-        "element": "c16df32e-03b5-47e4-868f-98d57eac1f15",
-        "name": "folio",
+        "element": "a0373c38-907d-4127-9762-5aada56b7165",
+        "name": "number",
         "type": "text",
-        "value": "1v",
+        "value": "2",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1669,12 +1669,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "bf7933a6-1646-40d0-be66-1306bc564481",
+    "pk": "e4b071a7-41f5-416f-aeee-53ca55d4b640",
     "fields": {
-        "element": "b8c4d1e3-aa9d-425a-b70f-c8ff16ae147c",
+        "element": "67482d70-765b-4092-b462-2d955fd8a7df",
         "name": "folio",
         "type": "text",
-        "value": "2r",
+        "value": "1r",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1682,12 +1682,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "cffe6d09-810c-4365-8107-af240d60097e",
+    "pk": "fec9c649-5fd4-43f7-ab28-3b364b868087",
     "fields": {
-        "element": "d227268a-2c2e-40ff-b22c-ab6e52eb5bba",
-        "name": "number",
+        "element": "7ac6504c-4088-4e6e-9a48-567f1f7e54c8",
+        "name": "folio",
         "type": "text",
-        "value": "1",
+        "value": "1v",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1710,12 +1710,12 @@
 },
 {
     "model": "images.image",
-    "pk": "2552f45d-6880-4a4c-b9c2-1091598a52b4",
+    "pk": "4867c2a1-fadc-4086-842c-c914b678d37d",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img4",
+        "path": "img5",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1724,12 +1724,12 @@
 },
 {
     "model": "images.image",
-    "pk": "47f164d3-d26f-41e8-9df1-b538645d60b5",
+    "pk": "627a53a6-466c-4114-b826-852ecd328e47",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img2",
+        "path": "img1",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1738,12 +1738,12 @@
 },
 {
     "model": "images.image",
-    "pk": "4b93eabe-b42d-4b88-8f31-a63d0258000a",
+    "pk": "68df44c1-ed1f-4f29-870d-de7b4b03b1a5",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img6",
+        "path": "img3",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1752,12 +1752,12 @@
 },
 {
     "model": "images.image",
-    "pk": "5de01fda-de62-4d26-b8bf-5c3f4678de3b",
+    "pk": "7f006318-b3d2-4cda-b1ab-8ff844c07b35",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img5",
+        "path": "img2",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1766,12 +1766,12 @@
 },
 {
     "model": "images.image",
-    "pk": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
+    "pk": "a68c21d7-d24f-44d7-b134-3c50ee6ddba1",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img1",
+        "path": "img4",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1780,12 +1780,12 @@
 },
 {
     "model": "images.image",
-    "pk": "d4db6162-7d7b-4839-a84f-756e74fef150",
+    "pk": "d7f4c84a-8aae-4c7d-86ce-567c5d4adc6d",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img3",
+        "path": "img6",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1794,56 +1794,56 @@
 },
 {
     "model": "users.right",
-    "pk": "066494da-9d28-44a8-ad04-0616cde80af0",
+    "pk": "053efd1b-d12f-49e5-ae72-54c7008224ec",
     "fields": {
         "user": 2,
         "group": null,
-        "content_type": 20,
-        "content_id": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+        "content_type": 19,
+        "content_id": "1204ee59-2b68-4dfb-a083-3df4996c2337",
         "level": 100
     }
 },
 {
     "model": "users.right",
-    "pk": "58ce1f20-32a1-4507-8edd-28fff7a1a77f",
+    "pk": "3e267701-586a-45f5-adf3-20bab26fbaae",
     "fields": {
         "user": 2,
         "group": null,
-        "content_type": 12,
-        "content_id": "c19116b6-866f-4bb7-b447-3544629a8151",
-        "level": 10
+        "content_type": 34,
+        "content_id": "192deb9f-d37e-4edd-bb9b-2ae35c7ecba5",
+        "level": 100
     }
 },
 {
     "model": "users.right",
-    "pk": "9f94b10b-ffd6-4a87-885d-af494d41c850",
+    "pk": "7c7634a0-2cd6-4d54-9a1b-14b0c0390050",
     "fields": {
-        "user": 3,
+        "user": 2,
         "group": null,
-        "content_type": 35,
-        "content_id": "c104ee5f-be60-4cdd-a738-10bf24601682",
-        "level": 50
+        "content_type": 11,
+        "content_id": "94055f35-9ac9-44a0-ac5d-386e132bea2c",
+        "level": 10
     }
 },
 {
     "model": "users.right",
-    "pk": "b04e5fb5-dbca-4255-aae1-4c100c510c9d",
+    "pk": "9f79af1d-f49f-4fd7-8504-2553ceab64db",
     "fields": {
-        "user": 2,
+        "user": 3,
         "group": null,
-        "content_type": 35,
-        "content_id": "c104ee5f-be60-4cdd-a738-10bf24601682",
-        "level": 100
+        "content_type": 34,
+        "content_id": "192deb9f-d37e-4edd-bb9b-2ae35c7ecba5",
+        "level": 50
     }
 },
 {
     "model": "users.right",
-    "pk": "da8297fb-5484-4511-8d29-0e8d6cbccc49",
+    "pk": "f38dad9a-d7b5-43cc-8ec7-e437296a0ca4",
     "fields": {
         "user": 4,
         "group": null,
-        "content_type": 35,
-        "content_id": "c104ee5f-be60-4cdd-a738-10bf24601682",
+        "content_type": 34,
+        "content_id": "192deb9f-d37e-4edd-bb9b-2ae35c7ecba5",
         "level": 10
     }
 },
@@ -1851,7 +1851,7 @@
     "model": "users.user",
     "pk": 1,
     "fields": {
-        "password": "pbkdf2_sha256$390000$IL4asfhs96nzQKK9md5Axl$uXyWwT/if+W9OsJ+OeHkPZWm4xlQ7D1ep2NyjxSNOLs=",
+        "password": "pbkdf2_sha256$390000$uNaUvBneqUWhzAsyWIWTw0$2Q13vkqIe5EhzX9rxV94tnAAuzYxXL1OMkfD49k8j0U=",
         "last_login": null,
         "email": "root@root.fr",
         "display_name": "Admin",
@@ -1866,7 +1866,7 @@
     "model": "users.user",
     "pk": 2,
     "fields": {
-        "password": "pbkdf2_sha256$390000$RvCTxXTmAXRgZ29LJrofXG$vnWF7puXQBP+G8wcclsOrd2ZpHbiq7jC/kX7F31tSQQ=",
+        "password": "pbkdf2_sha256$390000$c0a4pOThTZIgnvLuFhk0M8$uY0aZIWY33541EueCEsJwiSn9JV4/xMmblh2ZB+iJIQ=",
         "last_login": null,
         "email": "user@user.fr",
         "display_name": "Test user",
@@ -1909,7 +1909,7 @@
 },
 {
     "model": "users.group",
-    "pk": "c104ee5f-be60-4cdd-a738-10bf24601682",
+    "pk": "192deb9f-d37e-4edd-bb9b-2ae35c7ecba5",
     "fields": {
         "name": "User group",
         "public": false,
@@ -1921,7 +1921,7 @@
     "pk": 1,
     "fields": {
         "name": "Can add log entry",
-        "content_type": 3,
+        "content_type": 1,
         "codename": "add_logentry"
     }
 },
@@ -1930,7 +1930,7 @@
     "pk": 2,
     "fields": {
         "name": "Can change log entry",
-        "content_type": 3,
+        "content_type": 1,
         "codename": "change_logentry"
     }
 },
@@ -1939,7 +1939,7 @@
     "pk": 3,
     "fields": {
         "name": "Can delete log entry",
-        "content_type": 3,
+        "content_type": 1,
         "codename": "delete_logentry"
     }
 },
@@ -1948,7 +1948,7 @@
     "pk": 4,
     "fields": {
         "name": "Can view log entry",
-        "content_type": 3,
+        "content_type": 1,
         "codename": "view_logentry"
     }
 },
@@ -1957,7 +1957,7 @@
     "pk": 5,
     "fields": {
         "name": "Can add permission",
-        "content_type": 4,
+        "content_type": 2,
         "codename": "add_permission"
     }
 },
@@ -1966,7 +1966,7 @@
     "pk": 6,
     "fields": {
         "name": "Can change permission",
-        "content_type": 4,
+        "content_type": 2,
         "codename": "change_permission"
     }
 },
@@ -1975,7 +1975,7 @@
     "pk": 7,
     "fields": {
         "name": "Can delete permission",
-        "content_type": 4,
+        "content_type": 2,
         "codename": "delete_permission"
     }
 },
@@ -1984,7 +1984,7 @@
     "pk": 8,
     "fields": {
         "name": "Can view permission",
-        "content_type": 4,
+        "content_type": 2,
         "codename": "view_permission"
     }
 },
@@ -1993,7 +1993,7 @@
     "pk": 9,
     "fields": {
         "name": "Can add group",
-        "content_type": 5,
+        "content_type": 3,
         "codename": "add_group"
     }
 },
@@ -2002,7 +2002,7 @@
     "pk": 10,
     "fields": {
         "name": "Can change group",
-        "content_type": 5,
+        "content_type": 3,
         "codename": "change_group"
     }
 },
@@ -2011,7 +2011,7 @@
     "pk": 11,
     "fields": {
         "name": "Can delete group",
-        "content_type": 5,
+        "content_type": 3,
         "codename": "delete_group"
     }
 },
@@ -2020,7 +2020,7 @@
     "pk": 12,
     "fields": {
         "name": "Can view group",
-        "content_type": 5,
+        "content_type": 3,
         "codename": "view_group"
     }
 },
@@ -2029,7 +2029,7 @@
     "pk": 13,
     "fields": {
         "name": "Can add content type",
-        "content_type": 6,
+        "content_type": 4,
         "codename": "add_contenttype"
     }
 },
@@ -2038,7 +2038,7 @@
     "pk": 14,
     "fields": {
         "name": "Can change content type",
-        "content_type": 6,
+        "content_type": 4,
         "codename": "change_contenttype"
     }
 },
@@ -2047,7 +2047,7 @@
     "pk": 15,
     "fields": {
         "name": "Can delete content type",
-        "content_type": 6,
+        "content_type": 4,
         "codename": "delete_contenttype"
     }
 },
@@ -2056,7 +2056,7 @@
     "pk": 16,
     "fields": {
         "name": "Can view content type",
-        "content_type": 6,
+        "content_type": 4,
         "codename": "view_contenttype"
     }
 },
@@ -2065,7 +2065,7 @@
     "pk": 17,
     "fields": {
         "name": "Can add session",
-        "content_type": 7,
+        "content_type": 5,
         "codename": "add_session"
     }
 },
@@ -2074,7 +2074,7 @@
     "pk": 18,
     "fields": {
         "name": "Can change session",
-        "content_type": 7,
+        "content_type": 5,
         "codename": "change_session"
     }
 },
@@ -2083,7 +2083,7 @@
     "pk": 19,
     "fields": {
         "name": "Can delete session",
-        "content_type": 7,
+        "content_type": 5,
         "codename": "delete_session"
     }
 },
@@ -2092,7 +2092,7 @@
     "pk": 20,
     "fields": {
         "name": "Can view session",
-        "content_type": 7,
+        "content_type": 5,
         "codename": "view_session"
     }
 },
@@ -2101,7 +2101,7 @@
     "pk": 21,
     "fields": {
         "name": "Can add Token",
-        "content_type": 8,
+        "content_type": 6,
         "codename": "add_token"
     }
 },
@@ -2110,7 +2110,7 @@
     "pk": 22,
     "fields": {
         "name": "Can change Token",
-        "content_type": 8,
+        "content_type": 6,
         "codename": "change_token"
     }
 },
@@ -2119,7 +2119,7 @@
     "pk": 23,
     "fields": {
         "name": "Can delete Token",
-        "content_type": 8,
+        "content_type": 6,
         "codename": "delete_token"
     }
 },
@@ -2128,7 +2128,7 @@
     "pk": 24,
     "fields": {
         "name": "Can view Token",
-        "content_type": 8,
+        "content_type": 6,
         "codename": "view_token"
     }
 },
@@ -2137,7 +2137,7 @@
     "pk": 25,
     "fields": {
         "name": "Can add token",
-        "content_type": 9,
+        "content_type": 7,
         "codename": "add_tokenproxy"
     }
 },
@@ -2146,7 +2146,7 @@
     "pk": 26,
     "fields": {
         "name": "Can change token",
-        "content_type": 9,
+        "content_type": 7,
         "codename": "change_tokenproxy"
     }
 },
@@ -2155,7 +2155,7 @@
     "pk": 27,
     "fields": {
         "name": "Can delete token",
-        "content_type": 9,
+        "content_type": 7,
         "codename": "delete_tokenproxy"
     }
 },
@@ -2164,7 +2164,7 @@
     "pk": 28,
     "fields": {
         "name": "Can view token",
-        "content_type": 9,
+        "content_type": 7,
         "codename": "view_tokenproxy"
     }
 },
@@ -2173,7 +2173,7 @@
     "pk": 29,
     "fields": {
         "name": "Access admin page",
-        "content_type": 10,
+        "content_type": 8,
         "codename": "view"
     }
 },
@@ -2182,7 +2182,7 @@
     "pk": 30,
     "fields": {
         "name": "Can add agent",
-        "content_type": 1,
+        "content_type": 9,
         "codename": "add_agent"
     }
 },
@@ -2191,7 +2191,7 @@
     "pk": 31,
     "fields": {
         "name": "Can change agent",
-        "content_type": 1,
+        "content_type": 9,
         "codename": "change_agent"
     }
 },
@@ -2200,7 +2200,7 @@
     "pk": 32,
     "fields": {
         "name": "Can delete agent",
-        "content_type": 1,
+        "content_type": 9,
         "codename": "delete_agent"
     }
 },
@@ -2209,7 +2209,7 @@
     "pk": 33,
     "fields": {
         "name": "Can view agent",
-        "content_type": 1,
+        "content_type": 9,
         "codename": "view_agent"
     }
 },
@@ -2218,7 +2218,7 @@
     "pk": 34,
     "fields": {
         "name": "Can add artifact",
-        "content_type": 11,
+        "content_type": 10,
         "codename": "add_artifact"
     }
 },
@@ -2227,7 +2227,7 @@
     "pk": 35,
     "fields": {
         "name": "Can change artifact",
-        "content_type": 11,
+        "content_type": 10,
         "codename": "change_artifact"
     }
 },
@@ -2236,7 +2236,7 @@
     "pk": 36,
     "fields": {
         "name": "Can delete artifact",
-        "content_type": 11,
+        "content_type": 10,
         "codename": "delete_artifact"
     }
 },
@@ -2245,7 +2245,7 @@
     "pk": 37,
     "fields": {
         "name": "Can view artifact",
-        "content_type": 11,
+        "content_type": 10,
         "codename": "view_artifact"
     }
 },
@@ -2254,7 +2254,7 @@
     "pk": 38,
     "fields": {
         "name": "Can add farm",
-        "content_type": 12,
+        "content_type": 11,
         "codename": "add_farm"
     }
 },
@@ -2263,7 +2263,7 @@
     "pk": 39,
     "fields": {
         "name": "Can change farm",
-        "content_type": 12,
+        "content_type": 11,
         "codename": "change_farm"
     }
 },
@@ -2272,7 +2272,7 @@
     "pk": 40,
     "fields": {
         "name": "Can delete farm",
-        "content_type": 12,
+        "content_type": 11,
         "codename": "delete_farm"
     }
 },
@@ -2281,7 +2281,7 @@
     "pk": 41,
     "fields": {
         "name": "Can view farm",
-        "content_type": 12,
+        "content_type": 11,
         "codename": "view_farm"
     }
 },
@@ -2290,7 +2290,7 @@
     "pk": 42,
     "fields": {
         "name": "Can add gpu",
-        "content_type": 13,
+        "content_type": 12,
         "codename": "add_gpu"
     }
 },
@@ -2299,7 +2299,7 @@
     "pk": 43,
     "fields": {
         "name": "Can change gpu",
-        "content_type": 13,
+        "content_type": 12,
         "codename": "change_gpu"
     }
 },
@@ -2308,7 +2308,7 @@
     "pk": 44,
     "fields": {
         "name": "Can delete gpu",
-        "content_type": 13,
+        "content_type": 12,
         "codename": "delete_gpu"
     }
 },
@@ -2317,7 +2317,7 @@
     "pk": 45,
     "fields": {
         "name": "Can view gpu",
-        "content_type": 13,
+        "content_type": 12,
         "codename": "view_gpu"
     }
 },
@@ -2326,7 +2326,7 @@
     "pk": 46,
     "fields": {
         "name": "Can add secret",
-        "content_type": 14,
+        "content_type": 13,
         "codename": "add_secret"
     }
 },
@@ -2335,7 +2335,7 @@
     "pk": 47,
     "fields": {
         "name": "Can change secret",
-        "content_type": 14,
+        "content_type": 13,
         "codename": "change_secret"
     }
 },
@@ -2344,7 +2344,7 @@
     "pk": 48,
     "fields": {
         "name": "Can delete secret",
-        "content_type": 14,
+        "content_type": 13,
         "codename": "delete_secret"
     }
 },
@@ -2353,7 +2353,7 @@
     "pk": 49,
     "fields": {
         "name": "Can view secret",
-        "content_type": 14,
+        "content_type": 13,
         "codename": "view_secret"
     }
 },
@@ -2362,7 +2362,7 @@
     "pk": 50,
     "fields": {
         "name": "Can add task",
-        "content_type": 15,
+        "content_type": 14,
         "codename": "add_task"
     }
 },
@@ -2371,7 +2371,7 @@
     "pk": 51,
     "fields": {
         "name": "Can change task",
-        "content_type": 15,
+        "content_type": 14,
         "codename": "change_task"
     }
 },
@@ -2380,7 +2380,7 @@
     "pk": 52,
     "fields": {
         "name": "Can delete task",
-        "content_type": 15,
+        "content_type": 14,
         "codename": "delete_task"
     }
 },
@@ -2389,7 +2389,7 @@
     "pk": 53,
     "fields": {
         "name": "Can view task",
-        "content_type": 15,
+        "content_type": 14,
         "codename": "view_task"
     }
 },
@@ -2397,1561 +2397,1525 @@
     "model": "auth.permission",
     "pk": 54,
     "fields": {
-        "name": "Can add agent user",
-        "content_type": 2,
-        "codename": "add_agentuser"
+        "name": "Can add image",
+        "content_type": 15,
+        "codename": "add_image"
     }
 },
 {
     "model": "auth.permission",
     "pk": 55,
     "fields": {
-        "name": "Can change agent user",
-        "content_type": 2,
-        "codename": "change_agentuser"
+        "name": "Can change image",
+        "content_type": 15,
+        "codename": "change_image"
     }
 },
 {
     "model": "auth.permission",
     "pk": 56,
     "fields": {
-        "name": "Can delete agent user",
-        "content_type": 2,
-        "codename": "delete_agentuser"
+        "name": "Can delete image",
+        "content_type": 15,
+        "codename": "delete_image"
     }
 },
 {
     "model": "auth.permission",
     "pk": 57,
     "fields": {
-        "name": "Can view agent user",
-        "content_type": 2,
-        "codename": "view_agentuser"
+        "name": "Can view image",
+        "content_type": 15,
+        "codename": "view_image"
     }
 },
 {
     "model": "auth.permission",
     "pk": 58,
     "fields": {
-        "name": "Can add image",
+        "name": "Can add image server",
         "content_type": 16,
-        "codename": "add_image"
-    }
-},
-{
-    "model": "auth.permission",
-    "pk": 59,
-    "fields": {
-        "name": "Can change image",
-        "content_type": 16,
-        "codename": "change_image"
-    }
-},
-{
-    "model": "auth.permission",
-    "pk": 60,
-    "fields": {
-        "name": "Can delete image",
-        "content_type": 16,
-        "codename": "delete_image"
-    }
-},
-{
-    "model": "auth.permission",
-    "pk": 61,
-    "fields": {
-        "name": "Can view image",
-        "content_type": 16,
-        "codename": "view_image"
-    }
-},
-{
-    "model": "auth.permission",
-    "pk": 62,
-    "fields": {
-        "name": "Can add image server",
-        "content_type": 17,
         "codename": "add_imageserver"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 63,
+    "pk": 59,
     "fields": {
         "name": "Can change image server",
-        "content_type": 17,
+        "content_type": 16,
         "codename": "change_imageserver"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 64,
+    "pk": 60,
     "fields": {
         "name": "Can delete image server",
-        "content_type": 17,
+        "content_type": 16,
         "codename": "delete_imageserver"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 65,
+    "pk": 61,
     "fields": {
         "name": "Can view image server",
-        "content_type": 17,
+        "content_type": 16,
         "codename": "view_imageserver"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 66,
+    "pk": 62,
     "fields": {
         "name": "Can add allowed meta data",
-        "content_type": 18,
+        "content_type": 17,
         "codename": "add_allowedmetadata"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 67,
+    "pk": 63,
     "fields": {
         "name": "Can change allowed meta data",
-        "content_type": 18,
+        "content_type": 17,
         "codename": "change_allowedmetadata"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 68,
+    "pk": 64,
     "fields": {
         "name": "Can delete allowed meta data",
-        "content_type": 18,
+        "content_type": 17,
         "codename": "delete_allowedmetadata"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 69,
+    "pk": 65,
     "fields": {
         "name": "Can view allowed meta data",
-        "content_type": 18,
+        "content_type": 17,
         "codename": "view_allowedmetadata"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 70,
+    "pk": 66,
     "fields": {
         "name": "Can add classification",
-        "content_type": 19,
+        "content_type": 18,
         "codename": "add_classification"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 71,
+    "pk": 67,
     "fields": {
         "name": "Can change classification",
-        "content_type": 19,
+        "content_type": 18,
         "codename": "change_classification"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 72,
+    "pk": 68,
     "fields": {
         "name": "Can delete classification",
-        "content_type": 19,
+        "content_type": 18,
         "codename": "delete_classification"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 73,
+    "pk": 69,
     "fields": {
         "name": "Can view classification",
-        "content_type": 19,
+        "content_type": 18,
         "codename": "view_classification"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 74,
+    "pk": 70,
     "fields": {
         "name": "Can add corpus",
-        "content_type": 20,
+        "content_type": 19,
         "codename": "add_corpus"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 75,
+    "pk": 71,
     "fields": {
         "name": "Can change corpus",
-        "content_type": 20,
+        "content_type": 19,
         "codename": "change_corpus"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 76,
+    "pk": 72,
     "fields": {
         "name": "Can delete corpus",
-        "content_type": 20,
+        "content_type": 19,
         "codename": "delete_corpus"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 77,
+    "pk": 73,
     "fields": {
         "name": "Can view corpus",
-        "content_type": 20,
+        "content_type": 19,
         "codename": "view_corpus"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 78,
+    "pk": 74,
     "fields": {
         "name": "Can add corpus export",
-        "content_type": 21,
+        "content_type": 20,
         "codename": "add_corpusexport"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 79,
+    "pk": 75,
     "fields": {
         "name": "Can change corpus export",
-        "content_type": 21,
+        "content_type": 20,
         "codename": "change_corpusexport"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 80,
+    "pk": 76,
     "fields": {
         "name": "Can delete corpus export",
-        "content_type": 21,
+        "content_type": 20,
         "codename": "delete_corpusexport"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 81,
+    "pk": 77,
     "fields": {
         "name": "Can view corpus export",
-        "content_type": 21,
+        "content_type": 20,
         "codename": "view_corpusexport"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 82,
+    "pk": 78,
     "fields": {
         "name": "Can add element",
-        "content_type": 22,
+        "content_type": 21,
         "codename": "add_element"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 83,
+    "pk": 79,
     "fields": {
         "name": "Can change element",
-        "content_type": 22,
+        "content_type": 21,
         "codename": "change_element"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 84,
+    "pk": 80,
     "fields": {
         "name": "Can delete element",
-        "content_type": 22,
+        "content_type": 21,
         "codename": "delete_element"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 85,
+    "pk": 81,
     "fields": {
         "name": "Can view element",
-        "content_type": 22,
+        "content_type": 21,
         "codename": "view_element"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 86,
+    "pk": 82,
     "fields": {
         "name": "Can add element path",
-        "content_type": 23,
+        "content_type": 22,
         "codename": "add_elementpath"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 87,
+    "pk": 83,
     "fields": {
         "name": "Can change element path",
-        "content_type": 23,
+        "content_type": 22,
         "codename": "change_elementpath"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 88,
+    "pk": 84,
     "fields": {
         "name": "Can delete element path",
-        "content_type": 23,
+        "content_type": 22,
         "codename": "delete_elementpath"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 89,
+    "pk": 85,
     "fields": {
         "name": "Can view element path",
-        "content_type": 23,
+        "content_type": 22,
         "codename": "view_elementpath"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 90,
+    "pk": 86,
     "fields": {
         "name": "Can add element type",
-        "content_type": 24,
+        "content_type": 23,
         "codename": "add_elementtype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 91,
+    "pk": 87,
     "fields": {
         "name": "Can change element type",
-        "content_type": 24,
+        "content_type": 23,
         "codename": "change_elementtype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 92,
+    "pk": 88,
     "fields": {
         "name": "Can delete element type",
-        "content_type": 24,
+        "content_type": 23,
         "codename": "delete_elementtype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 93,
+    "pk": 89,
     "fields": {
         "name": "Can view element type",
-        "content_type": 24,
+        "content_type": 23,
         "codename": "view_elementtype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 94,
+    "pk": 90,
     "fields": {
         "name": "Can add entity",
-        "content_type": 25,
+        "content_type": 24,
         "codename": "add_entity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 95,
+    "pk": 91,
     "fields": {
         "name": "Can change entity",
-        "content_type": 25,
+        "content_type": 24,
         "codename": "change_entity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 96,
+    "pk": 92,
     "fields": {
         "name": "Can delete entity",
-        "content_type": 25,
+        "content_type": 24,
         "codename": "delete_entity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 97,
+    "pk": 93,
     "fields": {
         "name": "Can view entity",
-        "content_type": 25,
+        "content_type": 24,
         "codename": "view_entity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 98,
+    "pk": 94,
     "fields": {
         "name": "Can add entity link",
-        "content_type": 26,
+        "content_type": 25,
         "codename": "add_entitylink"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 99,
+    "pk": 95,
     "fields": {
         "name": "Can change entity link",
-        "content_type": 26,
+        "content_type": 25,
         "codename": "change_entitylink"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 100,
+    "pk": 96,
     "fields": {
         "name": "Can delete entity link",
-        "content_type": 26,
+        "content_type": 25,
         "codename": "delete_entitylink"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 101,
+    "pk": 97,
     "fields": {
         "name": "Can view entity link",
-        "content_type": 26,
+        "content_type": 25,
         "codename": "view_entitylink"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 102,
+    "pk": 98,
     "fields": {
         "name": "Can add entity role",
-        "content_type": 27,
+        "content_type": 26,
         "codename": "add_entityrole"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 103,
+    "pk": 99,
     "fields": {
         "name": "Can change entity role",
-        "content_type": 27,
+        "content_type": 26,
         "codename": "change_entityrole"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 104,
+    "pk": 100,
     "fields": {
         "name": "Can delete entity role",
-        "content_type": 27,
+        "content_type": 26,
         "codename": "delete_entityrole"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 105,
+    "pk": 101,
     "fields": {
         "name": "Can view entity role",
-        "content_type": 27,
+        "content_type": 26,
         "codename": "view_entityrole"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 106,
+    "pk": 102,
     "fields": {
         "name": "Can add entity type",
-        "content_type": 28,
+        "content_type": 27,
         "codename": "add_entitytype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 107,
+    "pk": 103,
     "fields": {
         "name": "Can change entity type",
-        "content_type": 28,
+        "content_type": 27,
         "codename": "change_entitytype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 108,
+    "pk": 104,
     "fields": {
         "name": "Can delete entity type",
-        "content_type": 28,
+        "content_type": 27,
         "codename": "delete_entitytype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 109,
+    "pk": 105,
     "fields": {
         "name": "Can view entity type",
-        "content_type": 28,
+        "content_type": 27,
         "codename": "view_entitytype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 110,
+    "pk": 106,
     "fields": {
         "name": "Can add meta data",
-        "content_type": 29,
+        "content_type": 28,
         "codename": "add_metadata"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 111,
+    "pk": 107,
     "fields": {
         "name": "Can change meta data",
-        "content_type": 29,
+        "content_type": 28,
         "codename": "change_metadata"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 112,
+    "pk": 108,
     "fields": {
         "name": "Can delete meta data",
-        "content_type": 29,
+        "content_type": 28,
         "codename": "delete_metadata"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 113,
+    "pk": 109,
     "fields": {
         "name": "Can view meta data",
-        "content_type": 29,
+        "content_type": 28,
         "codename": "view_metadata"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 114,
+    "pk": 110,
     "fields": {
         "name": "Can add ml class",
-        "content_type": 30,
+        "content_type": 29,
         "codename": "add_mlclass"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 115,
+    "pk": 111,
     "fields": {
         "name": "Can change ml class",
-        "content_type": 30,
+        "content_type": 29,
         "codename": "change_mlclass"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 116,
+    "pk": 112,
     "fields": {
         "name": "Can delete ml class",
-        "content_type": 30,
+        "content_type": 29,
         "codename": "delete_mlclass"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 117,
+    "pk": 113,
     "fields": {
         "name": "Can view ml class",
-        "content_type": 30,
+        "content_type": 29,
         "codename": "view_mlclass"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 118,
+    "pk": 114,
     "fields": {
         "name": "Can add selection",
-        "content_type": 31,
+        "content_type": 30,
         "codename": "add_selection"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 119,
+    "pk": 115,
     "fields": {
         "name": "Can change selection",
-        "content_type": 31,
+        "content_type": 30,
         "codename": "change_selection"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 120,
+    "pk": 116,
     "fields": {
         "name": "Can delete selection",
-        "content_type": 31,
+        "content_type": 30,
         "codename": "delete_selection"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 121,
+    "pk": 117,
     "fields": {
         "name": "Can view selection",
-        "content_type": 31,
+        "content_type": 30,
         "codename": "view_selection"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 122,
+    "pk": 118,
     "fields": {
         "name": "Can add transcription",
-        "content_type": 32,
+        "content_type": 31,
         "codename": "add_transcription"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 123,
+    "pk": 119,
     "fields": {
         "name": "Can change transcription",
-        "content_type": 32,
+        "content_type": 31,
         "codename": "change_transcription"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 124,
+    "pk": 120,
     "fields": {
         "name": "Can delete transcription",
-        "content_type": 32,
+        "content_type": 31,
         "codename": "delete_transcription"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 125,
+    "pk": 121,
     "fields": {
         "name": "Can view transcription",
-        "content_type": 32,
+        "content_type": 31,
         "codename": "view_transcription"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 126,
+    "pk": 122,
     "fields": {
         "name": "Can add transcription entity",
-        "content_type": 33,
+        "content_type": 32,
         "codename": "add_transcriptionentity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 127,
+    "pk": 123,
     "fields": {
         "name": "Can change transcription entity",
-        "content_type": 33,
+        "content_type": 32,
         "codename": "change_transcriptionentity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 128,
+    "pk": 124,
     "fields": {
         "name": "Can delete transcription entity",
-        "content_type": 33,
+        "content_type": 32,
         "codename": "delete_transcriptionentity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 129,
+    "pk": 125,
     "fields": {
         "name": "Can view transcription entity",
-        "content_type": 33,
+        "content_type": 32,
         "codename": "view_transcriptionentity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 130,
+    "pk": 126,
     "fields": {
         "name": "Can add user",
-        "content_type": 34,
+        "content_type": 33,
         "codename": "add_user"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 131,
+    "pk": 127,
     "fields": {
         "name": "Can change user",
-        "content_type": 34,
+        "content_type": 33,
         "codename": "change_user"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 132,
+    "pk": 128,
     "fields": {
         "name": "Can delete user",
-        "content_type": 34,
+        "content_type": 33,
         "codename": "delete_user"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 133,
+    "pk": 129,
     "fields": {
         "name": "Can view user",
-        "content_type": 34,
+        "content_type": 33,
         "codename": "view_user"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 134,
+    "pk": 130,
     "fields": {
         "name": "Can add group",
-        "content_type": 35,
+        "content_type": 34,
         "codename": "add_group"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 135,
+    "pk": 131,
     "fields": {
         "name": "Can change group",
-        "content_type": 35,
+        "content_type": 34,
         "codename": "change_group"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 136,
+    "pk": 132,
     "fields": {
         "name": "Can delete group",
-        "content_type": 35,
+        "content_type": 34,
         "codename": "delete_group"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 137,
+    "pk": 133,
     "fields": {
         "name": "Can view group",
-        "content_type": 35,
+        "content_type": 34,
         "codename": "view_group"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 138,
+    "pk": 134,
     "fields": {
         "name": "Can add right",
-        "content_type": 36,
+        "content_type": 35,
         "codename": "add_right"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 139,
+    "pk": 135,
     "fields": {
         "name": "Can change right",
-        "content_type": 36,
+        "content_type": 35,
         "codename": "change_right"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 140,
+    "pk": 136,
     "fields": {
         "name": "Can delete right",
-        "content_type": 36,
+        "content_type": 35,
         "codename": "delete_right"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 141,
+    "pk": 137,
     "fields": {
         "name": "Can view right",
-        "content_type": 36,
+        "content_type": 35,
         "codename": "view_right"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 142,
+    "pk": 138,
     "fields": {
         "name": "Can add user scope",
-        "content_type": 37,
+        "content_type": 36,
         "codename": "add_userscope"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 143,
+    "pk": 139,
     "fields": {
         "name": "Can change user scope",
-        "content_type": 37,
+        "content_type": 36,
         "codename": "change_userscope"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 144,
+    "pk": 140,
     "fields": {
         "name": "Can delete user scope",
-        "content_type": 37,
+        "content_type": 36,
         "codename": "delete_userscope"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 145,
+    "pk": 141,
     "fields": {
         "name": "Can view user scope",
-        "content_type": 37,
+        "content_type": 36,
         "codename": "view_userscope"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 146,
+    "pk": 142,
     "fields": {
         "name": "Can add corpus worker version",
-        "content_type": 38,
+        "content_type": 37,
         "codename": "add_corpusworkerversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 147,
+    "pk": 143,
     "fields": {
         "name": "Can change corpus worker version",
-        "content_type": 38,
+        "content_type": 37,
         "codename": "change_corpusworkerversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 148,
+    "pk": 144,
     "fields": {
         "name": "Can delete corpus worker version",
-        "content_type": 38,
+        "content_type": 37,
         "codename": "delete_corpusworkerversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 149,
+    "pk": 145,
     "fields": {
         "name": "Can view corpus worker version",
-        "content_type": 38,
+        "content_type": 37,
         "codename": "view_corpusworkerversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 150,
+    "pk": 146,
     "fields": {
         "name": "Can add data file",
-        "content_type": 39,
+        "content_type": 38,
         "codename": "add_datafile"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 151,
+    "pk": 147,
     "fields": {
         "name": "Can change data file",
-        "content_type": 39,
+        "content_type": 38,
         "codename": "change_datafile"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 152,
+    "pk": 148,
     "fields": {
         "name": "Can delete data file",
-        "content_type": 39,
+        "content_type": 38,
         "codename": "delete_datafile"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 153,
+    "pk": 149,
     "fields": {
         "name": "Can view data file",
-        "content_type": 39,
+        "content_type": 38,
         "codename": "view_datafile"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 154,
+    "pk": 150,
     "fields": {
         "name": "Can add git ref",
-        "content_type": 40,
+        "content_type": 39,
         "codename": "add_gitref"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 155,
+    "pk": 151,
     "fields": {
         "name": "Can change git ref",
-        "content_type": 40,
+        "content_type": 39,
         "codename": "change_gitref"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 156,
+    "pk": 152,
     "fields": {
         "name": "Can delete git ref",
-        "content_type": 40,
+        "content_type": 39,
         "codename": "delete_gitref"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 157,
+    "pk": 153,
     "fields": {
         "name": "Can view git ref",
-        "content_type": 40,
+        "content_type": 39,
         "codename": "view_gitref"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 158,
+    "pk": 154,
     "fields": {
         "name": "Can add process",
-        "content_type": 41,
+        "content_type": 40,
         "codename": "add_process"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 159,
+    "pk": 155,
     "fields": {
         "name": "Can change process",
-        "content_type": 41,
+        "content_type": 40,
         "codename": "change_process"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 160,
+    "pk": 156,
     "fields": {
         "name": "Can delete process",
-        "content_type": 41,
+        "content_type": 40,
         "codename": "delete_process"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 161,
+    "pk": 157,
     "fields": {
         "name": "Can view process",
-        "content_type": 41,
+        "content_type": 40,
         "codename": "view_process"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 162,
+    "pk": 158,
     "fields": {
         "name": "Can add process element",
-        "content_type": 42,
+        "content_type": 41,
         "codename": "add_processelement"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 163,
+    "pk": 159,
     "fields": {
         "name": "Can change process element",
-        "content_type": 42,
+        "content_type": 41,
         "codename": "change_processelement"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 164,
+    "pk": 160,
     "fields": {
         "name": "Can delete process element",
-        "content_type": 42,
+        "content_type": 41,
         "codename": "delete_processelement"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 165,
+    "pk": 161,
     "fields": {
         "name": "Can view process element",
-        "content_type": 42,
+        "content_type": 41,
         "codename": "view_processelement"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 166,
+    "pk": 162,
     "fields": {
         "name": "Can add repository",
-        "content_type": 43,
+        "content_type": 42,
         "codename": "add_repository"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 167,
+    "pk": 163,
     "fields": {
         "name": "Can change repository",
-        "content_type": 43,
+        "content_type": 42,
         "codename": "change_repository"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 168,
+    "pk": 164,
     "fields": {
         "name": "Can delete repository",
-        "content_type": 43,
+        "content_type": 42,
         "codename": "delete_repository"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 169,
+    "pk": 165,
     "fields": {
         "name": "Can view repository",
-        "content_type": 43,
+        "content_type": 42,
         "codename": "view_repository"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 170,
+    "pk": 166,
     "fields": {
         "name": "Can add revision",
-        "content_type": 44,
+        "content_type": 43,
         "codename": "add_revision"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 171,
+    "pk": 167,
     "fields": {
         "name": "Can change revision",
-        "content_type": 44,
+        "content_type": 43,
         "codename": "change_revision"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 172,
+    "pk": 168,
     "fields": {
         "name": "Can delete revision",
-        "content_type": 44,
+        "content_type": 43,
         "codename": "delete_revision"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 173,
+    "pk": 169,
     "fields": {
         "name": "Can view revision",
-        "content_type": 44,
+        "content_type": 43,
         "codename": "view_revision"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 174,
+    "pk": 170,
     "fields": {
         "name": "Can add worker",
-        "content_type": 45,
+        "content_type": 44,
         "codename": "add_worker"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 175,
+    "pk": 171,
     "fields": {
         "name": "Can change worker",
-        "content_type": 45,
+        "content_type": 44,
         "codename": "change_worker"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 176,
+    "pk": 172,
     "fields": {
         "name": "Can delete worker",
-        "content_type": 45,
+        "content_type": 44,
         "codename": "delete_worker"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 177,
+    "pk": 173,
     "fields": {
         "name": "Can view worker",
-        "content_type": 45,
+        "content_type": 44,
         "codename": "view_worker"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 178,
+    "pk": 174,
     "fields": {
         "name": "Can add worker activity",
-        "content_type": 46,
+        "content_type": 45,
         "codename": "add_workeractivity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 179,
+    "pk": 175,
     "fields": {
         "name": "Can change worker activity",
-        "content_type": 46,
+        "content_type": 45,
         "codename": "change_workeractivity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 180,
+    "pk": 176,
     "fields": {
         "name": "Can delete worker activity",
-        "content_type": 46,
+        "content_type": 45,
         "codename": "delete_workeractivity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 181,
+    "pk": 177,
     "fields": {
         "name": "Can view worker activity",
-        "content_type": 46,
+        "content_type": 45,
         "codename": "view_workeractivity"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 182,
+    "pk": 178,
     "fields": {
         "name": "Can add worker configuration",
-        "content_type": 47,
+        "content_type": 46,
         "codename": "add_workerconfiguration"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 183,
+    "pk": 179,
     "fields": {
         "name": "Can change worker configuration",
-        "content_type": 47,
+        "content_type": 46,
         "codename": "change_workerconfiguration"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 184,
+    "pk": 180,
     "fields": {
         "name": "Can delete worker configuration",
-        "content_type": 47,
+        "content_type": 46,
         "codename": "delete_workerconfiguration"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 185,
+    "pk": 181,
     "fields": {
         "name": "Can view worker configuration",
-        "content_type": 47,
+        "content_type": 46,
         "codename": "view_workerconfiguration"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 186,
+    "pk": 182,
     "fields": {
         "name": "Can add worker type",
-        "content_type": 48,
+        "content_type": 47,
         "codename": "add_workertype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 187,
+    "pk": 183,
     "fields": {
         "name": "Can change worker type",
-        "content_type": 48,
+        "content_type": 47,
         "codename": "change_workertype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 188,
+    "pk": 184,
     "fields": {
         "name": "Can delete worker type",
-        "content_type": 48,
+        "content_type": 47,
         "codename": "delete_workertype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 189,
+    "pk": 185,
     "fields": {
         "name": "Can view worker type",
-        "content_type": 48,
+        "content_type": 47,
         "codename": "view_workertype"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 190,
+    "pk": 186,
     "fields": {
         "name": "Can add worker version",
-        "content_type": 49,
+        "content_type": 48,
         "codename": "add_workerversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 191,
+    "pk": 187,
     "fields": {
         "name": "Can change worker version",
-        "content_type": 49,
+        "content_type": 48,
         "codename": "change_workerversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 192,
+    "pk": 188,
     "fields": {
         "name": "Can delete worker version",
-        "content_type": 49,
+        "content_type": 48,
         "codename": "delete_workerversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 193,
+    "pk": 189,
     "fields": {
         "name": "Can view worker version",
-        "content_type": 49,
+        "content_type": 48,
         "codename": "view_workerversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 194,
+    "pk": 190,
     "fields": {
         "name": "Can add worker run",
-        "content_type": 50,
+        "content_type": 49,
         "codename": "add_workerrun"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 195,
+    "pk": 191,
     "fields": {
         "name": "Can change worker run",
-        "content_type": 50,
+        "content_type": 49,
         "codename": "change_workerrun"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 196,
+    "pk": 192,
     "fields": {
         "name": "Can delete worker run",
-        "content_type": 50,
+        "content_type": 49,
         "codename": "delete_workerrun"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 197,
+    "pk": 193,
     "fields": {
         "name": "Can view worker run",
-        "content_type": 50,
+        "content_type": 49,
         "codename": "view_workerrun"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 198,
+    "pk": 194,
     "fields": {
         "name": "Can add process dataset",
-        "content_type": 51,
+        "content_type": 50,
         "codename": "add_processdataset"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 199,
+    "pk": 195,
     "fields": {
         "name": "Can change process dataset",
-        "content_type": 51,
+        "content_type": 50,
         "codename": "change_processdataset"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 200,
+    "pk": 196,
     "fields": {
         "name": "Can delete process dataset",
-        "content_type": 51,
+        "content_type": 50,
         "codename": "delete_processdataset"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 201,
+    "pk": 197,
     "fields": {
         "name": "Can view process dataset",
-        "content_type": 51,
+        "content_type": 50,
         "codename": "view_processdataset"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 202,
+    "pk": 198,
     "fields": {
         "name": "Can add dataset",
-        "content_type": 52,
+        "content_type": 51,
         "codename": "add_dataset"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 203,
+    "pk": 199,
     "fields": {
         "name": "Can change dataset",
-        "content_type": 52,
+        "content_type": 51,
         "codename": "change_dataset"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 204,
+    "pk": 200,
     "fields": {
         "name": "Can delete dataset",
-        "content_type": 52,
+        "content_type": 51,
         "codename": "delete_dataset"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 205,
+    "pk": 201,
     "fields": {
         "name": "Can view dataset",
-        "content_type": 52,
+        "content_type": 51,
         "codename": "view_dataset"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 206,
+    "pk": 202,
     "fields": {
         "name": "Can add metric key",
-        "content_type": 53,
+        "content_type": 52,
         "codename": "add_metrickey"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 207,
+    "pk": 203,
     "fields": {
         "name": "Can change metric key",
-        "content_type": 53,
+        "content_type": 52,
         "codename": "change_metrickey"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 208,
+    "pk": 204,
     "fields": {
         "name": "Can delete metric key",
-        "content_type": 53,
+        "content_type": 52,
         "codename": "delete_metrickey"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 209,
+    "pk": 205,
     "fields": {
         "name": "Can view metric key",
-        "content_type": 53,
+        "content_type": 52,
         "codename": "view_metrickey"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 210,
+    "pk": 206,
     "fields": {
         "name": "Can add model",
-        "content_type": 54,
+        "content_type": 53,
         "codename": "add_model"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 211,
+    "pk": 207,
     "fields": {
         "name": "Can change model",
-        "content_type": 54,
+        "content_type": 53,
         "codename": "change_model"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 212,
+    "pk": 208,
     "fields": {
         "name": "Can delete model",
-        "content_type": 54,
+        "content_type": 53,
         "codename": "delete_model"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 213,
+    "pk": 209,
     "fields": {
         "name": "Can view model",
-        "content_type": 54,
+        "content_type": 53,
         "codename": "view_model"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 214,
+    "pk": 210,
     "fields": {
         "name": "Can add model version",
-        "content_type": 55,
+        "content_type": 54,
         "codename": "add_modelversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 215,
+    "pk": 211,
     "fields": {
         "name": "Can change model version",
-        "content_type": 55,
+        "content_type": 54,
         "codename": "change_modelversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 216,
+    "pk": 212,
     "fields": {
         "name": "Can delete model version",
-        "content_type": 55,
+        "content_type": 54,
         "codename": "delete_modelversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 217,
+    "pk": 213,
     "fields": {
         "name": "Can view model version",
-        "content_type": 55,
+        "content_type": 54,
         "codename": "view_modelversion"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 218,
+    "pk": 214,
     "fields": {
         "name": "Can add metric value",
-        "content_type": 56,
+        "content_type": 55,
         "codename": "add_metricvalue"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 219,
+    "pk": 215,
     "fields": {
         "name": "Can change metric value",
-        "content_type": 56,
+        "content_type": 55,
         "codename": "change_metricvalue"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 220,
+    "pk": 216,
     "fields": {
         "name": "Can delete metric value",
-        "content_type": 56,
+        "content_type": 55,
         "codename": "delete_metricvalue"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 221,
+    "pk": 217,
     "fields": {
         "name": "Can view metric value",
-        "content_type": 56,
+        "content_type": 55,
         "codename": "view_metricvalue"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 222,
+    "pk": 218,
     "fields": {
         "name": "Can add dataset element",
-        "content_type": 57,
+        "content_type": 56,
         "codename": "add_datasetelement"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 223,
+    "pk": 219,
     "fields": {
         "name": "Can change dataset element",
-        "content_type": 57,
+        "content_type": 56,
         "codename": "change_datasetelement"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 224,
+    "pk": 220,
     "fields": {
         "name": "Can delete dataset element",
-        "content_type": 57,
+        "content_type": 56,
         "codename": "delete_datasetelement"
     }
 },
 {
     "model": "auth.permission",
-    "pk": 225,
+    "pk": 221,
     "fields": {
         "name": "Can view dataset element",
-        "content_type": 57,
+        "content_type": 56,
         "codename": "view_datasetelement"
     }
 },
 {
     "model": "ponos.farm",
-    "pk": "c19116b6-866f-4bb7-b447-3544629a8151",
+    "pk": "94055f35-9ac9-44a0-ac5d-386e132bea2c",
     "fields": {
         "name": "Wheat farm",
-        "seed": "03f762b3c505f0406a411b3fb3bbe4785c0e7763a05b515e7208b99ee93e955c"
+        "seed": "8cb2c985dcb2dcd344db193307b9049c1fe54d26faba25b417acbb2cdfcc9f41"
     }
 },
 {
     "model": "ponos.task",
-    "pk": "4ca6c03f-9fee-4d44-8b6d-a3cfa5ae1ffb",
+    "pk": "1cb97d66-b673-4068-9824-ab8c33fdcb95",
     "fields": {
         "run": 0,
         "depth": 0,
@@ -3967,22 +3931,22 @@
         "agent": null,
         "requires_gpu": false,
         "gpu": null,
-        "process": "245bc206-350c-43d5-8db3-d98645c4eaa9",
+        "process": "3421ba72-b14c-4df0-a504-1e7e90abe4b4",
         "worker_run": null,
         "container": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "expiry": "2100-12-31T23:59:59.999Z",
         "extra_files": "{}",
-        "token": "xm8g6R1OTqmlJVsQwA9aYZfHNbR2Q0fWmB1BRSy4plw=",
+        "token": "/Mp4N1XhQsWzkPneokooyFdtnCiW4EeQi09Z6nn5LUo=",
         "parents": []
     }
 },
 {
     "model": "ponos.artifact",
-    "pk": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+    "pk": "492ea45f-2c2d-4bf3-a461-016204fcfb03",
     "fields": {
-        "task": "4ca6c03f-9fee-4d44-8b6d-a3cfa5ae1ffb",
+        "task": "1cb97d66-b673-4068-9824-ab8c33fdcb95",
         "path": "/path/to/docker_build",
         "size": 42000,
         "content_type": "application/octet-stream",
@@ -3992,11 +3956,11 @@
 },
 {
     "model": "training.dataset",
-    "pk": "3c74b8de-7c42-4f2a-921f-a5ef957b80da",
+    "pk": "5fb84be2-a906-4bc0-830b-f3a369021a32",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
         "creator": 2,
         "task": null,
         "name": "Second Dataset",
@@ -4007,11 +3971,11 @@
 },
 {
     "model": "training.dataset",
-    "pk": "87c1ca55-bb92-4b6d-90af-a2f50ae49df9",
+    "pk": "cd5bcbb9-450f-4bdf-9bfc-9640194c2f88",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+        "corpus": "1204ee59-2b68-4dfb-a083-3df4996c2337",
         "creator": 2,
         "task": null,
         "name": "First Dataset",
diff --git a/arkindex/documents/tests/test_allowed_metadata.py b/arkindex/documents/tests/test_allowed_metadata.py
index b7c62412b1..00468e091a 100644
--- a/arkindex/documents/tests/test_allowed_metadata.py
+++ b/arkindex/documents/tests/test_allowed_metadata.py
@@ -1,3 +1,5 @@
+from unittest.mock import patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -51,7 +53,8 @@ class TestAllowedMetaData(FixtureAPITestCase):
             list(expected_meta[3:])
         )
 
-    def test_list_private_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_list_private_corpus(self, has_access_mock):
         with self.assertNumQueries(1):
             response = self.client.get(
                 reverse("api:corpus-allowed-metadata", kwargs={"pk": str(self.private_corpus.id)}),
@@ -77,7 +80,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
 
     def test_create(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:corpus-allowed-metadata", kwargs={"pk": str(self.corpus.id)}),
                 data={"name": "flan", "type": "text"}
@@ -93,7 +96,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
     def test_create_unique(self):
         self.client.force_login(self.user)
         self.corpus.allowed_metadatas.create(name="flan", type=MetaType.Text)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:corpus-allowed-metadata", kwargs={"pk": str(self.corpus.id)}),
                 data={"name": "flan", "type": "text"}
@@ -122,21 +125,21 @@ class TestAllowedMetaData(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_create_requires_corpus_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_requires_corpus_admin(self, has_access_mock):
         self.client.force_login(self.user)
-        for role in [Role.Guest, Role.Contributor]:
-            with self.subTest(role=role):
-                self.corpus.memberships.filter(user=self.user).update(level=role.value)
-                with self.assertNumQueries(5):
-                    response = self.client.post(
-                        reverse("api:corpus-allowed-metadata", kwargs={"pk": str(self.corpus.id)}),
-                        data={"name": "flan", "type": "text"}
-                    )
-                    self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_create_private_corpus(self):
+        self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
+        with self.assertNumQueries(3):
+            response = self.client.post(
+                reverse("api:corpus-allowed-metadata", kwargs={"pk": str(self.corpus.id)}),
+                data={"name": "flan", "type": "text"}
+            )
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_private_corpus(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-allowed-metadata", kwargs={"pk": str(self.private_corpus.id)}),
                 data={"name": "flan", "type": "text"}
@@ -145,7 +148,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
 
     def test_create_invalid(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-allowed-metadata", kwargs={"pk": str(self.corpus.id)}),
                 data={"name": "flan", "type": "pouet"}
@@ -175,9 +178,10 @@ class TestAllowedMetaData(FixtureAPITestCase):
             response = self.client.get(reverse("api:allowed-metadata-edit", kwargs={"corpus": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "pk": self.test_meta.id}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_retrieve_private_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_retrieve_private_corpus(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:allowed-metadata-edit", kwargs={"corpus": self.private_corpus.id, "pk": self.test_meta.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
@@ -228,9 +232,10 @@ class TestAllowedMetaData(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_update_private_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_update_private_corpus(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:allowed-metadata-edit", kwargs={"corpus": self.private_corpus.id, "pk": self.test_meta.id}),
                 {"type": "url", "name": "newName"},
@@ -239,7 +244,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
 
     def test_update_invalid_type(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
                 {"type": "invalidType", "name": "newName"},
@@ -251,7 +256,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
 
     def test_update_missing_argument(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
                 {"name": "newName"},
@@ -263,7 +268,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
 
     def test_update_duplicate(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
                 {"type": self.allowed_meta[1].type.value, "name": self.allowed_meta[1].name},
@@ -273,21 +278,20 @@ class TestAllowedMetaData(FixtureAPITestCase):
             "detail": ["An AllowedMetaData with this type and name already exists in this corpus."]
         })
 
-    def test_update_requires_corpus_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_update_requires_corpus_admin(self, has_access_mock):
         self.client.force_login(self.user)
-        for role in [Role.Guest, Role.Contributor]:
-            with self.subTest(role=role):
-                self.corpus.memberships.filter(user=self.user).update(level=role.value)
-                with self.assertNumQueries(5):
-                    response = self.client.put(
-                        reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
-                        {"type": self.allowed_meta[2].type.value, "name": self.allowed_meta[2].name},
-                    )
-                    self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
+        with self.assertNumQueries(3):
+            response = self.client.put(
+                reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
+                {"type": self.allowed_meta[2].type.value, "name": self.allowed_meta[2].name},
+            )
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
     def test_partial_update(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
                 {"name": "newName"},
@@ -317,9 +321,10 @@ class TestAllowedMetaData(FixtureAPITestCase):
                 {"type": "reference"})
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_partial_update_private_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_partial_update_private_corpus(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:allowed-metadata-edit", kwargs={"corpus": self.private_corpus.id, "pk": self.test_meta.id}),
                 {"type": "reference"})
@@ -327,7 +332,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
 
     def test_partial_update_invalid_type(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
                 {"type": "invalidType"},
@@ -339,7 +344,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
 
     def test_partial_update_duplicate(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.patch(
                 reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
                 {"type": self.allowed_meta[2].type.value},
@@ -349,21 +354,20 @@ class TestAllowedMetaData(FixtureAPITestCase):
             "detail": ["An AllowedMetaData with this type and name already exists in this corpus."]
         })
 
-    def test_partial_update_requires_corpus_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_partial_update_requires_corpus_admin(self, has_access_mock):
         self.client.force_login(self.user)
-        for role in [Role.Guest, Role.Contributor]:
-            with self.subTest(role=role):
-                self.corpus.memberships.filter(user=self.user).update(level=role.value)
-                with self.assertNumQueries(5):
-                    response = self.client.patch(
-                        reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
-                        {"type": self.allowed_meta[2].type.value},
-                    )
-                    self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
+        with self.assertNumQueries(3):
+            response = self.client.patch(
+                reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}),
+                {"type": self.allowed_meta[2].type.value},
+            )
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
     def test_destroy(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.delete(reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -372,7 +376,7 @@ class TestAllowedMetaData(FixtureAPITestCase):
 
     def test_destroy_not_found(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
@@ -395,17 +399,17 @@ class TestAllowedMetaData(FixtureAPITestCase):
             response = self.client.delete(reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_destroy_private_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_destroy_private_corpus(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:allowed-metadata-edit", kwargs={"corpus": self.private_corpus.id, "pk": self.test_meta.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_destroy_requires_corpus_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_destroy_requires_corpus_admin(self, has_access_mock):
         self.client.force_login(self.user)
-        for role in [Role.Guest, Role.Contributor]:
-            with self.subTest(role=role):
-                self.corpus.memberships.filter(user=self.user).update(level=role.value)
-                with self.assertNumQueries(5):
-                    response = self.client.delete(reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}))
-                    self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
+        with self.assertNumQueries(3):
+            response = self.client.delete(reverse("api:allowed-metadata-edit", kwargs={"corpus": self.corpus.id, "pk": self.test_meta.id}))
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
diff --git a/arkindex/documents/tests/test_bulk_classification.py b/arkindex/documents/tests/test_bulk_classification.py
index d722c54e41..ce5ab5e7d7 100644
--- a/arkindex/documents/tests/test_bulk_classification.py
+++ b/arkindex/documents/tests/test_bulk_classification.py
@@ -1,3 +1,5 @@
+from unittest.mock import patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -24,15 +26,17 @@ class TestBulkClassification(FixtureAPITestCase):
         response = self.client.post(reverse("api:classification-bulk"), format="json")
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_wrong_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_wrong_acl(self, filter_rights_mock):
         """
         The user must have access to the parent element
         """
+        filter_rights_mock.return_value = Corpus.objects.none()
         self.client.force_login(self.user)
         private_page = self.private_corpus.elements.create(
             type=self.private_corpus.types.create(slug="page"),
         )
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -61,7 +65,7 @@ class TestBulkClassification(FixtureAPITestCase):
         Classifications must be linked to a worker run
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -89,7 +93,7 @@ class TestBulkClassification(FixtureAPITestCase):
 
     def test_worker_run_not_found(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -119,7 +123,7 @@ class TestBulkClassification(FixtureAPITestCase):
         self.dog_class.delete()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -145,7 +149,7 @@ class TestBulkClassification(FixtureAPITestCase):
         Test the bulk classification API deletes previous classifications with the same worker run
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -205,7 +209,7 @@ class TestBulkClassification(FixtureAPITestCase):
         Test the bulk classification API prevents creating classifications with duplicate ML classes
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -297,7 +301,7 @@ class TestBulkClassification(FixtureAPITestCase):
         other_worker_run = process2.worker_runs.create(version=self.worker_run.version, parents=[])
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -325,7 +329,7 @@ class TestBulkClassification(FixtureAPITestCase):
         A regular user can create classifications with a WorkerRun of their own local process
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -396,7 +400,7 @@ class TestBulkClassification(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
@@ -445,7 +449,7 @@ class TestBulkClassification(FixtureAPITestCase):
 
         self.assertNotEqual(self.worker_run.process_id, local_worker_run.process_id)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:classification-bulk"),
                 format="json",
diff --git a/arkindex/documents/tests/test_bulk_element_transcriptions.py b/arkindex/documents/tests/test_bulk_element_transcriptions.py
index 8075296695..3ef2499efb 100644
--- a/arkindex/documents/tests/test_bulk_element_transcriptions.py
+++ b/arkindex/documents/tests/test_bulk_element_transcriptions.py
@@ -1,4 +1,5 @@
 import uuid
+from unittest.mock import patch
 
 from django.db.models import Count
 from django.test import override_settings
@@ -13,9 +14,6 @@ from arkindex.project.tests import FixtureAPITestCase
 
 
 class TestBulkElementTranscriptions(FixtureAPITestCase):
-    """
-    Tests for text element creation view
-    """
 
     @classmethod
     def setUpTestData(cls):
@@ -90,7 +88,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             } for poly, text, confidence, orientation in transcriptions]
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -150,7 +148,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             } for poly, text, confidence, orientation, element_confidence in transcriptions]
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -211,7 +209,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
         self.assertEqual(created_elts.count(), 1)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -253,7 +251,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
         }
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -302,7 +300,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
         }
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.huge_page.id}),
                 format="json",
@@ -334,12 +332,13 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_bulk_transcriptions_private(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_bulk_transcriptions_private(self, filter_rights_mock):
         """
         Accessing a non writeable element triggers a 404_NOT_FOUND
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.private_page.id}),
                 format="json",
@@ -372,7 +371,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
 
     def test_bulk_transcriptions_version_xor_run(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -469,7 +468,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
         }
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -536,7 +535,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             } for poly, text, confidence in transcriptions]
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": top_level.id}),
                 format="json",
@@ -584,7 +583,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             } for poly, text, confidence in transcriptions]
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -616,7 +615,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             } for poly, text, confidence, orientation in transcriptions]
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -647,7 +646,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             } for poly, text, confidence in transcriptions]
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.rotated_page.id}),
                 format="json",
@@ -684,7 +683,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             } for poly, text, confidence in transcriptions]
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.mirrored_page.id}),
                 format="json",
@@ -721,7 +720,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
             } for poly, text, confidence in transcriptions]
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -799,7 +798,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -832,7 +831,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(12):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
@@ -872,7 +871,7 @@ class TestBulkElementTranscriptions(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(12):
             response = self.client.post(
                 reverse("api:element-transcriptions-bulk", kwargs={"pk": self.page.id}),
                 format="json",
diff --git a/arkindex/documents/tests/test_bulk_elements.py b/arkindex/documents/tests/test_bulk_elements.py
index 2548632c65..21360cb428 100644
--- a/arkindex/documents/tests/test_bulk_elements.py
+++ b/arkindex/documents/tests/test_bulk_elements.py
@@ -1,10 +1,11 @@
+from unittest.mock import patch
 from uuid import uuid4
 
 from django.contrib.gis.geos import LineString
 from django.urls import reverse
 from rest_framework import status
 
-from arkindex.documents.models import Element, ElementPath
+from arkindex.documents.models import Corpus, Element, ElementPath
 from arkindex.process.models import ProcessMode, WorkerRun, WorkerVersion
 from arkindex.project.tests import FixtureAPITestCase
 from arkindex.users.models import Role
@@ -87,7 +88,8 @@ class TestBulkElements(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_bulk_create_writable_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_bulk_create_writable_corpus(self, filter_rights_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
         response = self.client.post(
@@ -114,7 +116,7 @@ class TestBulkElements(FixtureAPITestCase):
 
     def test_bulk_create_requires_elements(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data={
@@ -128,7 +130,7 @@ class TestBulkElements(FixtureAPITestCase):
 
     def test_bulk_create_missing_types(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data={
@@ -158,7 +160,7 @@ class TestBulkElements(FixtureAPITestCase):
         element_path1, element_path2 = self.element.paths.order_by("path__0")
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data=self.payload,
@@ -203,7 +205,7 @@ class TestBulkElements(FixtureAPITestCase):
 
         # Try to create a sub element on that zone
         self.client.force_login(self.user)
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(element.id)}),
                 data={
@@ -234,7 +236,7 @@ class TestBulkElements(FixtureAPITestCase):
 
     def test_bulk_create_negative_polygon(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 {
@@ -337,7 +339,7 @@ class TestBulkElements(FixtureAPITestCase):
                 }
             ]
         }
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data=payload,
@@ -354,7 +356,7 @@ class TestBulkElements(FixtureAPITestCase):
     def test_bulk_create_unknown_worker_run(self):
         self.client.force_login(self.user)
         random_uuid = str(uuid4())
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 {
@@ -379,7 +381,7 @@ class TestBulkElements(FixtureAPITestCase):
         Worker run attribute is required, worker version attribute is forbidden
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data={
@@ -448,7 +450,7 @@ class TestBulkElements(FixtureAPITestCase):
         task = self.worker_run.process.tasks.first()
         payload = {**self.payload, "worker_run_id": str(other_worker_run.id)}
 
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data=payload,
@@ -465,7 +467,7 @@ class TestBulkElements(FixtureAPITestCase):
         A regular user can create elements with a WorkerRun of their own local process
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data=self.payload,
@@ -514,7 +516,7 @@ class TestBulkElements(FixtureAPITestCase):
         task = self.worker_run.process.tasks.first()
         payload = {**self.payload, "worker_run_id": str(self.worker_run.id)}
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data=payload,
@@ -561,7 +563,7 @@ class TestBulkElements(FixtureAPITestCase):
 
         payload = {**self.payload, "worker_run_id": str(self.local_worker_run.id)}
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:elements-bulk-create", kwargs={"pk": str(self.element.id)}),
                 data=payload,
diff --git a/arkindex/documents/tests/test_bulk_transcription_entities.py b/arkindex/documents/tests/test_bulk_transcription_entities.py
index 6a49250e42..fc6b747da3 100644
--- a/arkindex/documents/tests/test_bulk_transcription_entities.py
+++ b/arkindex/documents/tests/test_bulk_transcription_entities.py
@@ -1,4 +1,5 @@
 import uuid
+from unittest.mock import patch
 
 from django.urls import reverse
 from rest_framework import status
@@ -39,10 +40,11 @@ class TestBulkTranscriptionEntities(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_requires_contributor(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_requires_contributor(self, has_access_mock):
         self.user.rights.update(level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:transcription-entities-bulk", kwargs={"pk": self.transcription.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {
@@ -96,7 +98,7 @@ class TestBulkTranscriptionEntities(FixtureAPITestCase):
             worker_version=self.local_worker_run.version,
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-entities-bulk", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -120,7 +122,7 @@ class TestBulkTranscriptionEntities(FixtureAPITestCase):
 
     def test_entity_fields_validation(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entities-bulk", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -150,7 +152,7 @@ class TestBulkTranscriptionEntities(FixtureAPITestCase):
             "length": 5,
             "confidence": 0.05,
         }
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-entities-bulk", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -242,7 +244,7 @@ class TestBulkTranscriptionEntities(FixtureAPITestCase):
         self.worker_run.process.run()
 
         task = self.worker_run.process.tasks.first()
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:transcription-entities-bulk", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -279,7 +281,7 @@ class TestBulkTranscriptionEntities(FixtureAPITestCase):
         )
         self.assertEqual(self.transcription.transcription_entities.count(), 0)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:transcription-entities-bulk", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -361,7 +363,7 @@ class TestBulkTranscriptionEntities(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.post(
                 reverse("api:transcription-entities-bulk", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -396,7 +398,7 @@ class TestBulkTranscriptionEntities(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.post(
                 reverse("api:transcription-entities-bulk", kwargs={"pk": str(self.transcription.id)}),
                 data={
diff --git a/arkindex/documents/tests/test_bulk_transcriptions.py b/arkindex/documents/tests/test_bulk_transcriptions.py
index ece36ea4cd..8c84c55a09 100644
--- a/arkindex/documents/tests/test_bulk_transcriptions.py
+++ b/arkindex/documents/tests/test_bulk_transcriptions.py
@@ -1,7 +1,9 @@
+from unittest.mock import patch
+
 from django.urls import reverse
 from rest_framework import status
 
-from arkindex.documents.models import TextOrientation
+from arkindex.documents.models import Corpus, TextOrientation
 from arkindex.process.models import ProcessMode, WorkerRun, WorkerVersion
 from arkindex.project.tests import FixtureAPITestCase
 
@@ -30,12 +32,13 @@ class TestBulkTranscriptions(FixtureAPITestCase):
             response = self.client.post(reverse("api:transcription-bulk"))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_bulk_transcriptions_not_found(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_bulk_transcriptions_not_found(self, filter_rights_mock):
         self.client.force_login(self.user)
         self.user.rights.all().delete()
         forbidden_element = self.corpus.elements.get(name="Volume 1, page 1r")
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:transcription-bulk"), {
                 "worker_run_id": str(self.local_worker_run.id),
                 "transcriptions": [
@@ -121,7 +124,7 @@ class TestBulkTranscriptions(FixtureAPITestCase):
         test_element = self.corpus.elements.get(name="Volume 2, page 1r")
         self.assertFalse(test_element.transcriptions.exists())
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:transcription-bulk"), {
                 "worker_run_id": str(self.local_worker_run.id),
                 "transcriptions": [
@@ -323,7 +326,7 @@ class TestBulkTranscriptions(FixtureAPITestCase):
         self.assertFalse(element1.transcriptions.exists())
         self.assertFalse(element2.transcriptions.exists())
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:transcription-bulk"), {
                 "worker_run_id": str(self.local_worker_run.id),
                 "transcriptions": [
@@ -413,7 +416,7 @@ class TestBulkTranscriptions(FixtureAPITestCase):
         element = self.corpus.elements.get(name="Volume 2, page 1r")
         self.assertFalse(element.transcriptions.exists())
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-bulk"),
                 format="json",
@@ -465,7 +468,7 @@ class TestBulkTranscriptions(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-bulk"),
                 format="json",
diff --git a/arkindex/documents/tests/test_classes.py b/arkindex/documents/tests/test_classes.py
index e0ff7d2c02..efe439164b 100644
--- a/arkindex/documents/tests/test_classes.py
+++ b/arkindex/documents/tests/test_classes.py
@@ -1,3 +1,4 @@
+from unittest.mock import patch
 
 from django.test import override_settings
 from django.urls import reverse
@@ -163,7 +164,8 @@ class TestClasses(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(response.json(), {"search": ["There cannot be more than 3 unique search terms."]})
 
-    def test_corpus_classes_corpus_rights(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_corpus_classes_corpus_rights(self, has_access_mock):
         self.client.force_login(self.user)
         private_corpus = Corpus.objects.create(name="private")
         response = self.client.post(reverse("api:corpus-classes", kwargs={"pk": private_corpus.pk}), {})
@@ -233,7 +235,8 @@ class TestClasses(FixtureAPITestCase):
         response = self.client.put(reverse("api:ml-class-retrieve", kwargs={"corpus": self.corpus.id, "mlclass": self.text.id}), {"name": "new name"})
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_update_requires_contributor(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_update_requires_contributor(self, has_access_mock):
         self.user.rights.update(level=Role.Guest.value)
         self.client.force_login(self.user)
         response = self.client.put(reverse("api:ml-class-retrieve", kwargs={"corpus": self.corpus.id, "mlclass": self.text.id}), {"name": "new name"})
@@ -262,7 +265,8 @@ class TestClasses(FixtureAPITestCase):
         response = self.client.patch(reverse("api:ml-class-retrieve", kwargs={"corpus": self.corpus.id, "mlclass": self.text.id}), {"name": "new name"})
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_partial_update_requires_contributor(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_partial_update_requires_contributor(self, has_access_mock):
         self.user.rights.update(level=Role.Guest.value)
         self.client.force_login(self.user)
         response = self.client.patch(reverse("api:ml-class-retrieve", kwargs={"corpus": self.corpus.id, "mlclass": self.text.id}), {"name": "new name"})
@@ -295,7 +299,8 @@ class TestClasses(FixtureAPITestCase):
         response = self.client.delete(reverse("api:ml-class-retrieve", kwargs={"corpus": self.corpus.id, "mlclass": self.text.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_destroy_requires_contributor(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_destroy_requires_contributor(self, has_access_mock):
         self.user.rights.update(level=Role.Guest.value)
         self.client.force_login(self.user)
         response = self.client.delete(reverse("api:ml-class-retrieve", kwargs={"corpus": self.corpus.id, "mlclass": self.text.id}))
diff --git a/arkindex/documents/tests/test_classification.py b/arkindex/documents/tests/test_classification.py
index 4270e08a16..04a2e5c434 100644
--- a/arkindex/documents/tests/test_classification.py
+++ b/arkindex/documents/tests/test_classification.py
@@ -1,4 +1,5 @@
 import uuid
+from unittest.mock import patch
 
 from django.test import override_settings
 from django.urls import reverse
@@ -31,7 +32,7 @@ class TestClassifications(FixtureAPITestCase):
         Creating a manual classification set auto fields correctly
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(self.text.id),
@@ -62,7 +63,7 @@ class TestClassifications(FixtureAPITestCase):
         A manual classification may be created specifying the version as null
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:classification-create"),
                 data={
@@ -84,7 +85,7 @@ class TestClassifications(FixtureAPITestCase):
             reverse("api:classification-create"),
             {"element": str(self.element.id), "ml_class": str(self.text.id)}
         )
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(*request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -124,7 +125,8 @@ class TestClassifications(FixtureAPITestCase):
             "non_field_errors": ["A classification from this worker run already exists for this element and this class."]
         })
 
-    def test_create_writable_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_create_writable_corpus(self, filter_rights_mock):
         """
         Require the element and ML class to be in a writable corpus
         """
@@ -133,7 +135,7 @@ class TestClassifications(FixtureAPITestCase):
         self.element.corpus = self.private_corpus
         self.element.save()
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(2):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(ml_class.id),
@@ -153,7 +155,7 @@ class TestClassifications(FixtureAPITestCase):
         self.private_corpus.memberships.create(user=self.user, level=Role.Contributor.value)
         ml_class = self.private_corpus.ml_classes.create(name="Heatmor")
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(ml_class.id),
@@ -170,7 +172,7 @@ class TestClassifications(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(self.text.id),
@@ -206,7 +208,7 @@ class TestClassifications(FixtureAPITestCase):
 
     def test_create_worker_version(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(self.text.id),
@@ -224,7 +226,7 @@ class TestClassifications(FixtureAPITestCase):
         A regular user can create a classification with a WorkerRun of their own local process
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(self.text.id),
@@ -261,7 +263,7 @@ class TestClassifications(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:classification-create"),
                 {
@@ -302,7 +304,7 @@ class TestClassifications(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:classification-create"),
                 {
@@ -389,7 +391,7 @@ class TestClassifications(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:classification-create"),
                 {
@@ -409,7 +411,7 @@ class TestClassifications(FixtureAPITestCase):
 
     def test_create_worker_version_xor_worker_run(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(self.text.id),
@@ -424,7 +426,7 @@ class TestClassifications(FixtureAPITestCase):
 
     def test_create_worker_run_not_found(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(self.text.id),
@@ -441,7 +443,7 @@ class TestClassifications(FixtureAPITestCase):
         Ensure CreateClassification accepts a confidence of 0
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(self.text.id),
@@ -477,7 +479,7 @@ class TestClassifications(FixtureAPITestCase):
         self.assertEqual(self.element.classifications.count(), 1)
 
         # Create a manual classification with the same class
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(reverse("api:classification-create"), {
                 "element": str(self.element.id),
                 "ml_class": str(self.text.id),
@@ -516,7 +518,7 @@ class TestClassifications(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.put(reverse("api:classification-validate", kwargs={"pk": classification.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -567,7 +569,7 @@ class TestClassifications(FixtureAPITestCase):
             confidence=.1,
         )
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.put(reverse("api:classification-reject", kwargs={"pk": classification.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -595,7 +597,7 @@ class TestClassifications(FixtureAPITestCase):
         self.client.force_login(self.user)
         classification = self.element.classifications.create(ml_class=self.text, confidence=.42)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.put(reverse("api:classification-reject", kwargs={"pk": classification.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -632,7 +634,7 @@ class TestClassifications(FixtureAPITestCase):
         )
 
         # First try to reject
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.put(reverse("api:classification-reject", kwargs={"pk": classification.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -686,7 +688,8 @@ class TestClassifications(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"non_field_errors": ["Selection is not available on this instance."]})
 
-    def test_create_selection_private_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_selection_private_corpus(self, has_access_mock):
         self.client.force_login(self.user)
         with self.assertNumQueries(5):
             response = self.client.post(
@@ -695,11 +698,12 @@ class TestClassifications(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
-    def test_create_selection_writable_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_selection_writable_corpus(self, has_access_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Guest.value)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:classification-selection"),
                 data={"corpus_id": self.corpus.id, "ml_class": self.text.id, "mode": "create"}
diff --git a/arkindex/documents/tests/test_corpus.py b/arkindex/documents/tests/test_corpus.py
index 4aeade12e3..884d60ccdb 100644
--- a/arkindex/documents/tests/test_corpus.py
+++ b/arkindex/documents/tests/test_corpus.py
@@ -1,4 +1,5 @@
 from datetime import datetime, timezone
+from unittest import expectedFailure
 from unittest.mock import call, patch
 from uuid import uuid4
 
@@ -73,6 +74,7 @@ class TestCorpus(FixtureAPITestCase):
             mock_now.return_value = FAKE_NOW
             cls.corpus_hidden = Corpus.objects.create(name="C Hidden")
 
+    @expectedFailure
     def test_anon(self):
         # An anonymous user has only access to public
         with self.assertNumQueries(4):
@@ -104,10 +106,11 @@ class TestCorpus(FixtureAPITestCase):
             ]
         )
 
+    @expectedFailure
     def test_user(self):
         # A normal user has access to public + its private
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(6):
             response = self.client.get(reverse("api:corpus"))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
@@ -222,6 +225,7 @@ class TestCorpus(FixtureAPITestCase):
         self.assertEqual(len(data), 13)
         self.assertSetEqual({corpus["top_level_type"] for corpus in data}, {None, "top_level"})
 
+    @expectedFailure
     def test_mixin(self):
         vol1 = Element.objects.get(name="Volume 1")
         vol2 = Element.objects.get(name="Volume 2")
@@ -293,7 +297,6 @@ class TestCorpus(FixtureAPITestCase):
         })
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         corpus = Corpus.objects.get(name="New Corpus", description="Some description", public=False)
-        self.assertNotIn(self.corpus_private, Corpus.objects.admin(self.user))
 
         # Assert defaults types are set on the new corpus
         self.assertCountEqual(
@@ -342,7 +345,7 @@ class TestCorpus(FixtureAPITestCase):
             "description": self.corpus_public.description,
             "public": True,
             "indexable": False,
-            "rights": ["read"],
+            "rights": ["read", "write", "admin"],
             "created": DB_CREATED,
             "authorized_users": 1,
             "top_level_type": None,
@@ -350,7 +353,7 @@ class TestCorpus(FixtureAPITestCase):
 
     def test_retrieve(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:corpus-retrieve", kwargs={"pk": self.corpus_private.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertDictEqual(response.json(), {
@@ -359,7 +362,7 @@ class TestCorpus(FixtureAPITestCase):
             "description": self.corpus_private.description,
             "public": False,
             "indexable": False,
-            "rights": ["read", "write"],
+            "rights": ["read", "write", "admin"],
             "types": [],
             "created": DB_CREATED,
             "authorized_users": 2,
@@ -371,15 +374,11 @@ class TestCorpus(FixtureAPITestCase):
         response = self.client.get(reverse("api:corpus-retrieve", kwargs={"pk": uuid4()}))
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_retrieve_requires_login(self):
-        response = self.client.get(reverse("api:corpus-retrieve", kwargs={"pk": self.corpus_private.id}))
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
     def test_retrieve_public_ignores_verified(self):
         self.user.verified_email = False
         self.user.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:corpus-retrieve", kwargs={"pk": self.corpus_public.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
@@ -399,6 +398,7 @@ class TestCorpus(FixtureAPITestCase):
             "top_level_type": None,
         })
 
+    @expectedFailure
     def test_retrieve_private_requires_guest(self):
         self.user.rights.all().delete()
         self.client.force_login(self.user)
@@ -482,6 +482,7 @@ class TestCorpus(FixtureAPITestCase):
         })
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_partial_update_requires_admin_right_on_corpus(self):
         for role in [Role.Guest, Role.Contributor]:
             with self.subTest(role=role):
@@ -591,6 +592,7 @@ class TestCorpus(FixtureAPITestCase):
         })
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_update_requires_admin_right_on_corpus(self):
         for role in [Role.Guest, Role.Contributor]:
             with self.subTest(role=role):
@@ -647,6 +649,7 @@ class TestCorpus(FixtureAPITestCase):
         response = self.client.delete(reverse("api:corpus-retrieve", kwargs={"pk": self.corpus_private.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_delete_requires_admin_right(self):
         for role in [Role.Guest, Role.Contributor]:
             with self.subTest(role=role):
@@ -655,6 +658,7 @@ class TestCorpus(FixtureAPITestCase):
                 response = self.client.delete(reverse("api:corpus-retrieve", kwargs={"pk": self.corpus_private.id}))
                 self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_delete_no_right_not_found(self):
         self.user.rights.all().delete()
         self.client.force_login(self.user)
@@ -706,6 +710,7 @@ class TestCorpus(FixtureAPITestCase):
         response = self.client.delete(reverse("api:corpus-delete-selection", kwargs={"pk": self.corpus_private.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_delete_selected_elements_requires_admin_right(self):
         self.assertNotIn(self.corpus_private, Corpus.objects.admin(self.user))
         self.client.force_login(self.user)
diff --git a/arkindex/documents/tests/test_corpus_authorized_users.py b/arkindex/documents/tests/test_corpus_authorized_users.py
index 2d913b9fca..8779b27b48 100644
--- a/arkindex/documents/tests/test_corpus_authorized_users.py
+++ b/arkindex/documents/tests/test_corpus_authorized_users.py
@@ -1,3 +1,5 @@
+from unittest import expectedFailure
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -68,6 +70,7 @@ class TestCorpusAuthorizedUsers(FixtureAPITestCase):
             ]
         )
 
+    @expectedFailure
     def test_user_one(self):
         self.client.force_login(self.user_one)
         response = self.client.get(reverse("api:corpus"))
@@ -103,6 +106,7 @@ class TestCorpusAuthorizedUsers(FixtureAPITestCase):
             ]
         )
 
+    @expectedFailure
     def test_user_two(self):
         self.client.force_login(self.user_two)
         response = self.client.get(reverse("api:corpus"))
@@ -133,6 +137,7 @@ class TestCorpusAuthorizedUsers(FixtureAPITestCase):
             ]
         )
 
+    @expectedFailure
     def test_user_three(self):
         self.client.force_login(self.user_three)
         response = self.client.get(reverse("api:corpus"))
diff --git a/arkindex/documents/tests/test_create_elements.py b/arkindex/documents/tests/test_create_elements.py
index 006f9c3179..b39fa86261 100644
--- a/arkindex/documents/tests/test_create_elements.py
+++ b/arkindex/documents/tests/test_create_elements.py
@@ -80,7 +80,7 @@ class TestCreateElements(FixtureAPITestCase):
         # Create a Volume
         self.client.force_login(self.user)
         request = self.make_create_request("my new volume")
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         data = response.json()
@@ -125,7 +125,7 @@ class TestCreateElements(FixtureAPITestCase):
             name="The castle of my dreams",
             image=str(self.image.id),
         )
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(12):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         data = response.json()
@@ -180,7 +180,7 @@ class TestCreateElements(FixtureAPITestCase):
             name="The castle of my dreams",
             polygon=polygon,
         )
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(12):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         data = response.json()
@@ -200,7 +200,7 @@ class TestCreateElements(FixtureAPITestCase):
             name="Castle story",
             elt_type="act"
         )
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(11):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         act = Element.objects.get(id=response.json()["id"])
@@ -216,7 +216,7 @@ class TestCreateElements(FixtureAPITestCase):
             name="The castle of my dreams again",
             polygon=[[10, 10], [10, 40], [40, 40], [40, 10], [10, 10]],
         )
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -232,7 +232,7 @@ class TestCreateElements(FixtureAPITestCase):
             image=str(self.image.id),
             polygon=[[0, 0], [10, 10], [40, 40], [-10, 10], [0, 0]]
         )
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -250,7 +250,7 @@ class TestCreateElements(FixtureAPITestCase):
             image=str(self.image.id),
             polygon=polygon
         )
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(12):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         data = response.json()
@@ -300,7 +300,7 @@ class TestCreateElements(FixtureAPITestCase):
             image=str(self.huge_image.id),
             polygon=polygon
         )
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(12):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json())
         self.assertListEqual(response.json()["zone"]["polygon"], polygon)
@@ -315,7 +315,7 @@ class TestCreateElements(FixtureAPITestCase):
             corpus=str(new_corpus.id),
             elt_type="act"
         )
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(5):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -336,7 +336,7 @@ class TestCreateElements(FixtureAPITestCase):
             elt_type="volume",
             name="something",
         )
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         new_vol = new_corpus.elements.get()
@@ -351,7 +351,7 @@ class TestCreateElements(FixtureAPITestCase):
             elt_type="kartoffelsalad",
             name="something",
         )
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -367,7 +367,7 @@ class TestCreateElements(FixtureAPITestCase):
         )
         self.image.status = S3FileStatus.Error
         self.image.save()
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(12):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         element = self.corpus.elements.get(id=response.json()["id"])
@@ -384,7 +384,7 @@ class TestCreateElements(FixtureAPITestCase):
         self.image.height = 0
         self.image.status = S3FileStatus.Unchecked
         self.image.save()
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"image": ["This image does not have valid dimensions."]})
@@ -395,7 +395,7 @@ class TestCreateElements(FixtureAPITestCase):
             name="slim output !",
         )
         request["path"] += "?slim_output=true"
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         element = self.corpus.elements.get(id=response.json()["id"])
@@ -412,7 +412,7 @@ class TestCreateElements(FixtureAPITestCase):
             image=str(self.image.id),
         )
         request["path"] += "?slim_output=true"
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(11):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         element = self.corpus.elements.get(id=response.json()["id"])
@@ -449,7 +449,7 @@ class TestCreateElements(FixtureAPITestCase):
                 if mirrored is not None:
                     request["data"]["mirrored"] = mirrored
 
-                with self.assertNumQueries(15):
+                with self.assertNumQueries(12):
                     response = self.client.post(**request)
                     self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -476,7 +476,7 @@ class TestCreateElements(FixtureAPITestCase):
                     rotation_angle=rotation_angle,
                 )
 
-                with self.assertNumQueries(8):
+                with self.assertNumQueries(6):
                     response = self.client.post(**request)
                     self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -497,7 +497,7 @@ class TestCreateElements(FixtureAPITestCase):
             image=str(self.image.id),
             polygon=polygon
         )
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -514,7 +514,7 @@ class TestCreateElements(FixtureAPITestCase):
             confidence=0.42,
         )
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(12):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -541,7 +541,7 @@ class TestCreateElements(FixtureAPITestCase):
                     confidence=confidence,
                 )
 
-                with self.assertNumQueries(8):
+                with self.assertNumQueries(6):
                     response = self.client.post(**request)
                     self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -555,7 +555,7 @@ class TestCreateElements(FixtureAPITestCase):
         """
         self.client.force_login(self.local_worker_run.process.creator)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:elements-create"),
                 {
@@ -582,7 +582,7 @@ class TestCreateElements(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:elements-create"),
                 {
@@ -617,7 +617,7 @@ class TestCreateElements(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:elements-create"),
                 {
@@ -699,7 +699,7 @@ class TestCreateElements(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:elements-create"),
                 {
@@ -724,7 +724,7 @@ class TestCreateElements(FixtureAPITestCase):
             elt_type="act",
             worker_run_id=random_uuid,
         )
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -738,7 +738,7 @@ class TestCreateElements(FixtureAPITestCase):
             worker_version=str(self.worker_version.id),
             worker_run_id=str(self.local_worker_run.id),
         )
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -755,7 +755,7 @@ class TestCreateElements(FixtureAPITestCase):
             worker_version=str(self.worker_version.id),
         )
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(**request)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
diff --git a/arkindex/documents/tests/test_create_parent_selection.py b/arkindex/documents/tests/test_create_parent_selection.py
index 64e562bb4f..f60e35e3e7 100644
--- a/arkindex/documents/tests/test_create_parent_selection.py
+++ b/arkindex/documents/tests/test_create_parent_selection.py
@@ -41,7 +41,7 @@ class TestMoveSelection(FixtureAPITestCase):
     @override_settings(ARKINDEX_FEATURES={"selection": False})
     def test_disabled(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:create-parent-selection"), {
                 "corpus_id": str(self.corpus.id),
                 "parent_id": str(self.parent.id),
@@ -52,14 +52,15 @@ class TestMoveSelection(FixtureAPITestCase):
             )
 
     @override_settings(ARKINDEX_FEATURES={"selection": True})
-    def test_wrong_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_wrong_acl(self, filter_rights_mock):
         private_corpus = Corpus.objects.create(name="private", public=False)
         private_element = private_corpus.elements.create(
             type=private_corpus.types.create(slug="folder"),
         )
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(2):
             response = self.client.post(reverse("api:create-parent-selection"), {
                 "corpus_id": str(private_corpus.id),
                 "parent_id": str(private_element.id),
@@ -77,7 +78,7 @@ class TestMoveSelection(FixtureAPITestCase):
     def test_wrong_parent(self):
         self.client.force_login(self.user)
         self.user.selected_elements.add(self.page)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:create-parent-selection"), {
                 "corpus_id": str(self.corpus.id),
                 "parent_id": "12341234-1234-1234-1234-123412341234"
@@ -92,7 +93,7 @@ class TestMoveSelection(FixtureAPITestCase):
     def test_same_parent(self):
         self.client.force_login(self.user)
         self.user.selected_elements.add(self.page)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(reverse("api:create-parent-selection"), {
                 "corpus_id": str(self.corpus.id),
                 "parent_id": str(self.page.id),
@@ -110,7 +111,7 @@ class TestMoveSelection(FixtureAPITestCase):
         parent = corpus2.elements.create(type=corpus2.types.create(slug="folder"))
         self.client.force_login(self.user)
         self.user.selected_elements.add(self.page)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:create-parent-selection"), {
                 "corpus_id": str(self.corpus.id),
                 "parent_id": str(parent.id),
@@ -126,7 +127,7 @@ class TestMoveSelection(FixtureAPITestCase):
         parent = self.corpus.elements.get(name="Volume 1")
         self.client.force_login(self.user)
         self.user.selected_elements.add(self.page)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(reverse("api:create-parent-selection"), {
                 "corpus_id": str(self.corpus.id),
                 "parent_id": str(parent.id),
@@ -143,7 +144,7 @@ class TestMoveSelection(FixtureAPITestCase):
         parent_id = self.page.id
         self.client.force_login(self.user)
         self.user.selected_elements.add(target)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.post(reverse("api:create-parent-selection"), {
                 "corpus_id": str(self.corpus.id),
                 "parent_id": str(parent_id),
@@ -161,7 +162,7 @@ class TestMoveSelection(FixtureAPITestCase):
         self.user.selected_elements.add(self.page)
         another_page = self.corpus.elements.get(name="Volume 1, page 1v")
         self.user.selected_elements.add(another_page)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.post(reverse("api:create-parent-selection"), {
                 "corpus_id": str(self.corpus.id),
                 "parent_id": str(self.parent.id),
diff --git a/arkindex/documents/tests/test_create_transcriptions.py b/arkindex/documents/tests/test_create_transcriptions.py
index f110bfeca9..d29c2f7f98 100644
--- a/arkindex/documents/tests/test_create_transcriptions.py
+++ b/arkindex/documents/tests/test_create_transcriptions.py
@@ -1,3 +1,4 @@
+from unittest.mock import patch
 from uuid import uuid4
 
 from django.test import override_settings
@@ -41,9 +42,10 @@ class TestTranscriptionCreate(FixtureAPITestCase):
             "detail": "Authentication credentials were not provided."
         })
 
-    def test_write_right(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_write_right(self, has_access_mock):
         self.client.force_login(self.private_read_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.private_page.id}),
                 format="json",
@@ -54,9 +56,10 @@ class TestTranscriptionCreate(FixtureAPITestCase):
             "detail": "A write access to the element's corpus is required."
         })
 
-    def test_no_read_right(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_no_read_right(self, filter_rights_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.private_page.id}),
                 format="json",
@@ -66,7 +69,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
 
     def test_no_element(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": uuid4()}),
                 format="json",
@@ -76,7 +79,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
 
     def test_manual(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -104,7 +107,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
         ts = self.line.transcriptions.create(text="GLOUBIBOULGA")
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -123,7 +126,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         Check that a transcription is created with the specified orientation
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -147,7 +150,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         Specifying an invalid text-orientation causes an error
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -158,7 +161,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
     @override_settings(ARKINDEX_FEATURES={"search": False})
     def test_no_search(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -168,7 +171,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
 
     def test_worker_version(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -189,7 +192,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -227,7 +230,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -263,7 +266,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -349,7 +352,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -368,7 +371,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
 
     def test_worker_run_not_found(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -386,7 +389,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
 
     def test_worker_run_required_confidence(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -402,7 +405,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
 
     def test_worker_version_xor_worker_run(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
@@ -424,7 +427,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         null_zone_page = self.corpus.elements.create(type=self.page.type)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": null_zone_page.id}),
                 format="json",
@@ -450,7 +453,7 @@ class TestTranscriptionCreate(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:transcription-create", kwargs={"pk": self.line.id}),
                 format="json",
diff --git a/arkindex/documents/tests/test_destroy_elements.py b/arkindex/documents/tests/test_destroy_elements.py
index f9403f3745..da6312ac19 100644
--- a/arkindex/documents/tests/test_destroy_elements.py
+++ b/arkindex/documents/tests/test_destroy_elements.py
@@ -30,14 +30,15 @@ class TestDestroyElements(FixtureAPITestCase):
             response = self.client.delete(reverse("api:element-retrieve", kwargs={"pk": str(self.vol.id)}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_element_destroy_acl(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_element_destroy_acl(self, has_access_mock):
         self.private_corpus.memberships.create(user=self.user, level=Role.Contributor.value)
         self.client.force_login(self.user)
         castle_story = self.private_corpus.elements.create(
             type=self.volume_type,
             name="Castle story"
         )
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:element-retrieve", kwargs={"pk": str(castle_story.id)}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(
@@ -53,7 +54,7 @@ class TestDestroyElements(FixtureAPITestCase):
             name="Castle story"
         )
         self.assertTrue(self.corpus.elements.filter(id=castle_story.id).exists())
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:element-retrieve", kwargs={"pk": str(castle_story.id)}))
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -80,7 +81,7 @@ class TestDestroyElements(FixtureAPITestCase):
             with self.subTest(delete_children=delete_children):
                 delay_mock.reset_mock()
 
-                with self.assertNumQueries(7):
+                with self.assertNumQueries(3):
                     response = self.client.delete(
                         reverse("api:element-retrieve", kwargs={"pk": str(castle_story.id)})
                         + f"?delete_children={delete_children}",
@@ -109,7 +110,7 @@ class TestDestroyElements(FixtureAPITestCase):
             name="Castle story",
             creator=self.user,
         )
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:element-retrieve", kwargs={"pk": str(castle_story.id)}))
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -123,7 +124,8 @@ class TestDestroyElements(FixtureAPITestCase):
             "description": "Element deletion",
         })
 
-    def test_element_destroy_creator_acl(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_element_destroy_creator_acl(self, has_access_mock):
         """
         An element's creator cannot delete without write access
         """
@@ -134,7 +136,7 @@ class TestDestroyElements(FixtureAPITestCase):
             name="Castle story",
             creator=self.user,
         )
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:element-retrieve", kwargs={"pk": str(castle_story.id)}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(
@@ -148,7 +150,7 @@ class TestDestroyElements(FixtureAPITestCase):
         """
         Dataset.objects.get(name="First Dataset").dataset_elements.create(element=self.vol, set="test")
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:element-retrieve", kwargs={"pk": str(self.vol.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You cannot delete an element that is part of a dataset."})
@@ -159,7 +161,7 @@ class TestDestroyElements(FixtureAPITestCase):
         We can now delete a non-empty element
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:element-retrieve", kwargs={"pk": str(self.vol.id)}))
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -316,11 +318,12 @@ class TestDestroyElements(FixtureAPITestCase):
         self.assertFalse(delay_mock.called)
 
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
-    def test_destroy_corpus_elements_requires_admin(self, delay_mock):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_destroy_corpus_elements_requires_admin(self, has_access_mock, delay_mock):
         self.corpus.memberships.update(level=Role.Contributor.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:corpus-elements", kwargs={"corpus": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have admin access to this corpus."})
@@ -332,7 +335,7 @@ class TestDestroyElements(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.assertFalse(self.corpus.elements.filter(name="blablablabla").exists())
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:corpus-elements", kwargs={"corpus": self.corpus.id}),
                 QUERY_STRING="name=blablablabla",
@@ -344,7 +347,7 @@ class TestDestroyElements(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
     def test_destroy_corpus_elements(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:corpus-elements", kwargs={"corpus": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -361,7 +364,7 @@ class TestDestroyElements(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
     def test_destroy_corpus_elements_delete_children(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:corpus-elements", kwargs={"corpus": self.corpus.id}),
                 QUERY_STRING="delete_children=false"
@@ -388,11 +391,12 @@ class TestDestroyElements(FixtureAPITestCase):
         self.assertFalse(delay_mock.called)
 
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
-    def test_destroy_element_children_requires_admin(self, delay_mock):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_destroy_element_children_requires_admin(self, has_access_mock, delay_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:elements-children", kwargs={"pk": self.vol.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have admin access to this corpus."})
@@ -404,7 +408,7 @@ class TestDestroyElements(FixtureAPITestCase):
         self.client.force_login(self.user)
         element = self.corpus.elements.create(type=self.volume_type, name="Lonely element")
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:elements-children", kwargs={"pk": element.id}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
@@ -413,7 +417,7 @@ class TestDestroyElements(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
     def test_destroy_element_children(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:elements-children", kwargs={"pk": self.vol.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -434,7 +438,7 @@ class TestDestroyElements(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
     def test_destroy_element_children_delete_children(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:elements-children", kwargs={"pk": self.vol.id}),
                 QUERY_STRING="delete_children=false"
@@ -465,11 +469,12 @@ class TestDestroyElements(FixtureAPITestCase):
         self.assertFalse(delay_mock.called)
 
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
-    def test_destroy_element_parents_requires_writable(self, delay_mock):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_destroy_element_parents_requires_writable(self, has_access_mock, delay_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:elements-parents", kwargs={"pk": self.surface.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have admin access to this corpus."})
@@ -481,7 +486,7 @@ class TestDestroyElements(FixtureAPITestCase):
         self.client.force_login(self.user)
         element = self.corpus.elements.create(type=self.volume_type, name="Lonely element")
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:elements-parents", kwargs={"pk": element.id}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
@@ -490,7 +495,7 @@ class TestDestroyElements(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
     def test_destroy_element_parents(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:elements-parents", kwargs={"pk": self.surface.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -511,7 +516,7 @@ class TestDestroyElements(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.element_trash.delay")
     def test_destroy_element_parents_delete_parents(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:elements-parents", kwargs={"pk": self.surface.id}),
                 QUERY_STRING="delete_children=false"
diff --git a/arkindex/documents/tests/test_destroy_worker_results.py b/arkindex/documents/tests/test_destroy_worker_results.py
index 2aedc94152..ee5b35614b 100644
--- a/arkindex/documents/tests/test_destroy_worker_results.py
+++ b/arkindex/documents/tests/test_destroy_worker_results.py
@@ -43,9 +43,10 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
             response = self.client.delete(reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_wrong_acl(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_wrong_acl(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:worker-delete-results", kwargs={"corpus": str(self.private_corpus.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(
@@ -66,7 +67,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.worker_results_delete.delay")
     def test_no_filter(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
@@ -85,7 +86,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.worker_results_delete.delay")
     def test_filter_version(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + f"?worker_version_id={self.version.id}",
@@ -111,7 +112,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
         are ignored
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + (
@@ -138,7 +139,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.worker_results_delete.delay")
     def test_filter_worker_run(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + f"?worker_run_id={self.worker_run.id}",
@@ -160,7 +161,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.worker_results_delete.delay")
     def test_filter_element(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + f"?element_id={self.page.id}"
@@ -182,7 +183,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.worker_results_delete.delay")
     def test_filter_element_worker_run(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + (
@@ -207,7 +208,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.worker_results_delete.delay")
     def test_filter_unset_configuration(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?configuration_id=false",
@@ -229,7 +230,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.worker_results_delete.delay")
     def test_filter_model_version(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + f"?model_version_id={self.model_version.id}",
@@ -251,7 +252,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.worker_results_delete.delay")
     def test_filter_element_worker_version_model_version_configuration(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + (
@@ -282,7 +283,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_invalid_version_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?worker_version_id=lol"
@@ -295,7 +296,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_wrong_version_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?worker_version_id=12341234-1234-1234-1234-123412341234"
@@ -308,7 +309,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_invalid_worker_run_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?worker_run_id=lol"
@@ -321,7 +322,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_wrong_worker_run_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?worker_run_id=12341234-1234-1234-1234-123412341234"
@@ -334,7 +335,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_invalid_element_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?element_id=lol"
@@ -347,7 +348,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_wrong_element_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?element_id=12341234-1234-1234-1234-123412341234"
@@ -360,7 +361,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_invalid_model_version_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?model_version_id=lol"
@@ -373,7 +374,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_wrong_model_version_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?model_version_id=12341234-1234-1234-1234-123412341234"
@@ -386,7 +387,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_invalid_configuration_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?configuration_id=true"
@@ -399,7 +400,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
 
     def test_wrong_configuration_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?configuration_id=12341234-1234-1234-1234-123412341234"
@@ -413,7 +414,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @override_settings(ARKINDEX_FEATURES={"selection": False})
     def test_selection_feature_flag(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?use_selection=true"
@@ -427,7 +428,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     @override_settings(ARKINDEX_FEATURES={"selection": True})
     def test_selection_no_element_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + f"?use_selection=true&element_id={self.page.id}"
@@ -445,7 +446,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
     def test_selection_empty(self):
         self.client.force_login(self.user)
         self.assertFalse(self.user.selected_elements.exists())
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?use_selection=true"
@@ -478,7 +479,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
         self.user.selected_elements.add(self.page)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + "?use_selection=true"
@@ -502,7 +503,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
         self.user.selected_elements.add(self.page)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + f"?use_selection=true&worker_version_id={self.version.id}"
@@ -527,7 +528,7 @@ class TestDestroyWorkerResults(FixtureAPITestCase):
         self.user.selected_elements.add(self.page)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.delete(
                 reverse("api:worker-delete-results", kwargs={"corpus": str(self.corpus.id)})
                 + f"?use_selection=true&worker_run_id={self.worker_run.id}"
diff --git a/arkindex/documents/tests/test_edit_transcriptions.py b/arkindex/documents/tests/test_edit_transcriptions.py
index a3341ab3de..c756ea5ddc 100644
--- a/arkindex/documents/tests/test_edit_transcriptions.py
+++ b/arkindex/documents/tests/test_edit_transcriptions.py
@@ -1,3 +1,4 @@
+from unittest.mock import patch
 from uuid import uuid4
 
 from django.urls import reverse
@@ -80,7 +81,8 @@ class TestEditTranscription(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
-    def test_get_private(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_get_private(self, filter_rights_mock):
         user = User.objects.create_user("f@gh.ij", "b")
         user.verified_email = True
         user.save()
@@ -183,7 +185,8 @@ class TestEditTranscription(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(response.json(), {"orientation": ["Value is not of type TextOrientation"]})
 
-    def test_patch_write_right(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_patch_write_right(self, has_access_mock):
         """
         A write right is required to patch a manual transcription
         """
@@ -198,7 +201,8 @@ class TestEditTranscription(FixtureAPITestCase):
             "detail": "A write access to transcription element corpus is required."
         })
 
-    def test_patch_admin_right(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_patch_admin_right(self, has_access_mock):
         """
         Updating a transcription produced by a ML worker is forbidden
         """
@@ -281,7 +285,8 @@ class TestEditTranscription(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(response.json(), {"orientation": ["Value is not of type TextOrientation"]})
 
-    def test_put_write_right(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_put_write_right(self, has_access_mock):
         """
         A write right is required to put a manual transcription
         """
@@ -296,7 +301,8 @@ class TestEditTranscription(FixtureAPITestCase):
             "detail": "A write access to transcription element corpus is required."
         })
 
-    def test_put_admin_right(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_put_admin_right(self, has_access_mock):
         """
         Updating a transcription produced by a ML worker is forbidden
         """
@@ -346,7 +352,8 @@ class TestEditTranscription(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         self.assertFalse(Transcription.objects.filter(id=self.manual_transcription.id).exists())
 
-    def test_delete_manual_write_right(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_delete_manual_write_right(self, has_access_mock):
         """
         Deleting a transcription without a write right is forbidden
         """
diff --git a/arkindex/documents/tests/test_element_paths_api.py b/arkindex/documents/tests/test_element_paths_api.py
index 12667ba439..f71d1298e3 100644
--- a/arkindex/documents/tests/test_element_paths_api.py
+++ b/arkindex/documents/tests/test_element_paths_api.py
@@ -1,3 +1,5 @@
+from unittest.mock import patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -41,7 +43,7 @@ class TestElementsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
 
         # Link desk to its parent room
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(11):
             response = self.client.post(reverse("api:element-parent", kwargs=self.default_kwargs))
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -65,10 +67,13 @@ class TestElementsAPI(FixtureAPITestCase):
         response = self.client.post(reverse("api:element-parent", kwargs=self.default_kwargs))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_create_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_create_acl(self, filter_rights_mock):
         self.client.force_login(self.user)
         self.desk.corpus = self.private_corpus
         self.desk.save()
+        filter_rights_mock.return_value = Corpus.objects.filter(id=self.corpus.id)
+
         response = self.client.post(reverse("api:element-parent", kwargs=self.default_kwargs))
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -131,10 +136,13 @@ class TestElementsAPI(FixtureAPITestCase):
         response = self.client.delete(reverse("api:element-parent", kwargs=self.default_kwargs))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_delete_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_delete_acl(self, filter_rights_mock):
         self.client.force_login(self.user)
         self.desk.corpus = self.private_corpus
         self.desk.save()
+        filter_rights_mock.return_value = Corpus.objects.filter(id=self.corpus.id)
+
         response = self.client.delete(reverse("api:element-parent", kwargs=self.default_kwargs))
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
diff --git a/arkindex/documents/tests/test_element_type.py b/arkindex/documents/tests/test_element_type.py
index bed5b21d49..37d2882b3c 100644
--- a/arkindex/documents/tests/test_element_type.py
+++ b/arkindex/documents/tests/test_element_type.py
@@ -1,3 +1,5 @@
+from unittest.mock import call, patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -23,7 +25,8 @@ class TestElementType(FixtureAPITestCase):
         }, format="json")
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_create_no_access(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_create_requires_admin(self, filter_rights_mock):
         self.client.force_login(self.user)
         response = self.client.post(reverse("api:element-type-create"), {
             "corpus": self.private_corpus.id,
@@ -32,15 +35,8 @@ class TestElementType(FixtureAPITestCase):
         }, format="json")
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
-    def test_create_requires_admin(self):
-        self.private_corpus.memberships.create(user=self.user, level=Role.Contributor.value)
-        self.client.force_login(self.user)
-        response = self.client.post(reverse("api:element-type-create"), {
-            "corpus": self.private_corpus.id,
-            "slug": "New_element_type",
-            "display_name": "New element type",
-        }, format="json")
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Admin.value))
 
     def test_create_wrong_slug(self):
         self.client.force_login(self.superuser)
@@ -153,16 +149,8 @@ class TestElementType(FixtureAPITestCase):
         }, format="json")
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_partial_update_no_access(self):
-        private_element_type = ElementType.objects.create(corpus=self.private_corpus, slug="element", display_name="Element")
-        self.client.force_login(self.user)
-        response = self.client.patch(reverse("api:element-type", kwargs={"pk": private_element_type.id}), {
-            "slug": "New_element_type",
-            "display_name": "New element type",
-        }, format="json")
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
-    def test_partial_update_requires_admin(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_partial_update_requires_admin(self, filter_rights_mock):
         private_element_type = ElementType.objects.create(corpus=self.private_corpus, slug="element", display_name="Element")
         self.private_corpus.memberships.create(user=self.user, level=Role.Contributor.value)
         self.client.force_login(self.user)
@@ -172,6 +160,9 @@ class TestElementType(FixtureAPITestCase):
         }, format="json")
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Admin.value))
+
     def test_partial_update_wrong_slug(self):
         element_type = ElementType.objects.create(corpus=self.corpus, slug="new_type", display_name="Element")
         self.client.force_login(self.superuser)
@@ -254,16 +245,8 @@ class TestElementType(FixtureAPITestCase):
         }, format="json")
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_update_no_access(self):
-        private_element_type = ElementType.objects.create(corpus=self.private_corpus, slug="element", display_name="Element")
-        self.client.force_login(self.user)
-        response = self.client.put(reverse("api:element-type", kwargs={"pk": private_element_type.id}), {
-            "slug": "New_element_type",
-            "display_name": "New element type",
-        }, format="json")
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
-    def test_update_requires_admin(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_update_requires_admin(self, filter_rights_mock):
         private_element_type = ElementType.objects.create(corpus=self.private_corpus, slug="element", display_name="Element")
         self.private_corpus.memberships.create(user=self.user, level=Role.Contributor.value)
         self.client.force_login(self.user)
@@ -273,6 +256,9 @@ class TestElementType(FixtureAPITestCase):
         }, format="json")
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Admin.value))
+
     def test_update_wrong_slug(self):
         element_type = ElementType.objects.create(corpus=self.corpus, slug="new_type", display_name="Element")
         self.client.force_login(self.superuser)
@@ -341,13 +327,17 @@ class TestElementType(FixtureAPITestCase):
         response = self.client.delete(reverse("api:element-type", kwargs={"pk": self.element_type.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_delete_requires_admin(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_delete_requires_admin(self, filter_rights_mock):
         private_element_type = ElementType.objects.create(corpus=self.private_corpus, slug="element", display_name="Element")
         self.private_corpus.memberships.create(user=self.user, level=Role.Contributor.value)
         self.client.force_login(self.user)
         response = self.client.delete(reverse("api:element-type", kwargs={"pk": private_element_type.id}))
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Admin.value))
+
     def test_delete(self):
         self.client.force_login(self.superuser)
         with self.assertNumQueries(11):
diff --git a/arkindex/documents/tests/test_entities_api.py b/arkindex/documents/tests/test_entities_api.py
index 39c4dde2fd..28dcd2b9f6 100644
--- a/arkindex/documents/tests/test_entities_api.py
+++ b/arkindex/documents/tests/test_entities_api.py
@@ -1,5 +1,7 @@
 import uuid
+from unittest.mock import call, patch
 
+from django.contrib.auth.models import AnonymousUser
 from django.contrib.gis.geos import LinearRing
 from django.urls import reverse
 from rest_framework import status
@@ -16,6 +18,7 @@ from arkindex.documents.models import (
 )
 from arkindex.process.models import ProcessMode, WorkerRun, WorkerVersion
 from arkindex.project.tests import FixtureAPITestCase
+from arkindex.users.models import Role
 
 
 class TestEntitiesAPI(FixtureAPITestCase):
@@ -98,7 +101,8 @@ class TestEntitiesAPI(FixtureAPITestCase):
     def test_get_entity(self):
         with self.assertNumQueries(3):
             response = self.client.get(reverse("api:entity-details", kwargs={"pk": str(self.entity.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         data = response.json()
         self.assertEqual(data["id"], str(self.entity.id))
         self.assertEqual(data["type"]["id"], str(self.entity.type.id))
@@ -108,21 +112,25 @@ class TestEntitiesAPI(FixtureAPITestCase):
     def test_get_entity_elements(self):
         with self.assertNumQueries(6):
             response = self.client.get(reverse("api:entity-elements", kwargs={"pk": str(self.entity.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         data = response.json()
         results = data["results"]
         self.assertEqual(len(results), 1)
         self.assertEqual(results[0]["id"], str(self.element.id))
         self.assertEqual(results[0]["name"], self.element.name)
 
-    def test_get_entity_elements_corpus_acl(self):
-        self.client.force_login(self.user)
-        self.element.corpus = self.private_corpus
-        self.element.save()
-        with self.assertNumQueries(5):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_get_entity_elements_corpus_acl(self, filter_rights_mock):
+        with self.assertNumQueries(0):
             response = self.client.get(reverse("api:entity-elements", kwargs={"pk": str(self.entity.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertListEqual(response.json().get("results"), [])
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(AnonymousUser(), Corpus, Role.Guest.value),
+            call(AnonymousUser(), Corpus, Role.Guest.value),
+        ])
 
     def test_get_entity_elements_from_transcription(self):
         elt = self.corpus.elements.create(
@@ -133,9 +141,11 @@ class TestEntitiesAPI(FixtureAPITestCase):
         )
         elt_tr = elt.transcriptions.create(worker_version=self.worker_version_1, text="goodbye")
         TranscriptionEntity.objects.create(transcription=elt_tr, entity=self.entity, offset=42, length=7)
+
         with self.assertNumQueries(6):
             response = self.client.get(reverse("api:entity-elements", kwargs={"pk": str(self.entity.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         data = response.json()
         results = data["results"]
         self.assertEqual(len(results), 2)
@@ -200,7 +210,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             "child_type_id": self.location_type.id
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(reverse("api:corpus-roles", kwargs={"pk": str(self.corpus.id)}), data=data)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         data = response.json()
@@ -219,7 +229,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             "child_type_id": self.role.child_type.id,
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(reverse("api:corpus-roles", kwargs={"pk": str(self.corpus.id)}), data=data)
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         data = response.json()
@@ -254,27 +264,6 @@ class TestEntitiesAPI(FixtureAPITestCase):
             response = self.client.post(reverse("api:corpus-roles", kwargs={"pk": str(self.corpus.id)}), data=data)
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_create_role_no_corpus_rights(self):
-        self.client.force_login(self.user)
-
-        private_corpus = Corpus.objects.create(name="private")
-        org_type = EntityType.objects.create(name="organization", corpus=private_corpus)
-        location_type = EntityType.objects.create(name="location", corpus=private_corpus)
-        data = {
-            "parent_name": "other parent",
-            "child_name": "other child",
-            "parent_type_id": org_type.id,
-            "child_type_id": location_type.id
-        }
-        with self.assertNumQueries(7):
-            response = self.client.post(reverse("api:corpus-roles", kwargs={"pk": str(private_corpus.id)}), data=data)
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        data = response.json()
-        self.assertEqual(data, {
-            "corpus": ["You do not have write access to this corpus"],
-            "id": [str(private_corpus.id)]
-        })
-
     def test_create_role_type_not_in_corpus(self):
         private_corpus = Corpus.objects.create(name="private")
         ext_type_1 = EntityType.objects.create(name="goose", corpus=private_corpus)
@@ -286,7 +275,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             "parent_name": "nick",
             "child_name": "bradley"
         }
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:corpus-roles", kwargs={"pk": str(self.corpus.id)}), data=data)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -305,7 +294,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             },
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:entity-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"type_id": ["“location” is not a valid UUID."]})
@@ -321,7 +310,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             },
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:entity-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"type_id": ['Invalid pk "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - object does not exist.']})
@@ -338,7 +327,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             },
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:entity-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"type_id": [f"EntityType {str(extraneous_type.id)} does not exist in corpus Unit Tests."]})
@@ -353,7 +342,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             },
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:entity-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"type_id": ["This field is required."]})
@@ -387,7 +376,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             },
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:entity-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(response.json(), {
@@ -406,7 +395,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             },
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:entity-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"worker_run_id": ['Invalid pk "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - object does not exist.']})
@@ -416,7 +405,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         A user can create an entity without a worker run
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:entity-create"),
                 format="json",
@@ -502,7 +491,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         self.worker_run_1.process.run()
         task = self.worker_run_1.process.tasks.first()
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:entity-create"),
                 format="json",
@@ -536,7 +525,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             },
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.post(reverse("api:entity-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -588,7 +577,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         self.worker_run_1.process.run()
         task = self.worker_run_1.process.tasks.first()
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:entity-create"),
                 format="json",
@@ -623,7 +612,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         self.worker_run_1.process.run()
         task = self.worker_run_1.process.tasks.first()
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:entity-create"),
                 format="json",
@@ -660,7 +649,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             "role": str(self.role.id)
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(16):
+        with self.assertNumQueries(14):
             response = self.client.post(reverse("api:entity-link-create"), data=data, format="json")
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         link = EntityLink.objects.get(id=response.json()["id"])
@@ -697,13 +686,13 @@ class TestEntitiesAPI(FixtureAPITestCase):
             "role": str(self.role.id)
         }
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(9):
             response = self.client.post(reverse("api:entity-link-create"), data=data, format="json")
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
     def test_create_transcription_entity(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data=self.tr_entities_sample,
@@ -730,7 +719,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_create_transcription_entity_with_confidence(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data=self.tr_entities_confidence_sample,
@@ -767,7 +756,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_create_transcription_entity_worker_version(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -788,7 +777,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         A regular user can create classifications with a WorkerRun of their own local process
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -813,7 +802,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_create_transcription_entity_forbidden_version(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -831,7 +820,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_create_transcription_entity_bad_worker_run(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -847,25 +836,33 @@ class TestEntitiesAPI(FixtureAPITestCase):
             "worker_run_id": ['Invalid pk "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - object does not exist.'],
         })
 
-    def test_create_transcription_entity_wrong_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_create_transcription_entity_wrong_acl(self, filter_rights_mock):
         self.client.force_login(self.user)
         self.element.corpus = self.private_corpus
         self.element.save()
-        with self.assertNumQueries(7):
+        filter_rights_mock.return_value = Corpus.objects.exclude(id=self.private_corpus.id)
+
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data=self.tr_entities_sample,
                 format="json"
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertDictEqual(
             response.json(),
             {"transcription": ["invalid UUID or Corpus write-access is forbidden"]}
         )
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Corpus, Role.Contributor.value),
+            call(self.user, Corpus, Role.Contributor.value),
+        ])
 
     def test_create_transcription_entity_wrong_length(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -883,7 +880,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_create_transcription_entity_wrong_high_confidence(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -902,7 +899,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_create_transcription_entity_wrong_low_confidence(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -919,7 +916,8 @@ class TestEntitiesAPI(FixtureAPITestCase):
             {"confidence": ["Ensure this value is greater than or equal to 0."]}
         )
 
-    def test_create_transcription_entity_different_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_create_transcription_entity_different_corpus(self, filter_rights_mock):
         self.client.force_login(self.user)
         person_type = EntityType.objects.create(name="person", corpus=self.private_corpus)
         ent = Entity.objects.create(
@@ -929,17 +927,24 @@ class TestEntitiesAPI(FixtureAPITestCase):
             worker_version=self.worker_version_1,
         )
         self.tr_entities_sample.update({"entity": ent.id})
-        with self.assertNumQueries(6):
+        filter_rights_mock.return_value = Corpus.objects.exclude(id=self.private_corpus.id)
+
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data=self.tr_entities_sample,
                 format="json"
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertDictEqual(
             response.json(),
             {"entity": [f'Invalid pk "{ent.id}" - object does not exist.']}
         )
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Corpus, Role.Contributor.value),
+            call(self.user, Corpus, Role.Contributor.value),
+        ])
 
     def test_create_transcription_entity_duplicate(self):
         self.client.force_login(self.user)
@@ -949,7 +954,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             offset=4,
             length=len(self.entity.name)
         )
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data=self.tr_entities_sample,
@@ -974,7 +979,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         )
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -1051,7 +1056,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         self.worker_run_1.process.run()
         task = self.worker_run_1.process.tasks.first()
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 format="json",
@@ -1078,7 +1083,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         self.worker_run_1.process.run()
         task = self.worker_run_1.process.tasks.first()
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 format="json",
@@ -1113,7 +1118,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         self.worker_run_1.process.run()
         task = self.worker_run_1.process.tasks.first()
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 format="json",
@@ -1150,7 +1155,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             worker_run=self.worker_run_1,
             worker_version=self.worker_version_1
         )
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data={
@@ -1177,7 +1182,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         data = self.tr_entities_sample
         del data["length"]
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:transcription-entity-create", kwargs={"pk": str(self.transcription.id)}),
                 data=data,
@@ -1189,7 +1194,8 @@ class TestEntitiesAPI(FixtureAPITestCase):
         response = self.client.get(reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
 
-    def test_list_transcription_entities_wrong_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_list_transcription_entities_wrong_acl(self, filter_rights_mock):
         self.transcription.element.corpus = self.private_corpus
         self.transcription.element.save()
         TranscriptionEntity.objects.create(
@@ -1199,12 +1205,17 @@ class TestEntitiesAPI(FixtureAPITestCase):
             length=len(self.entity.name)
         )
         self.client.force_login(self.user)
-        response = self.client.get(reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}))
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        with self.assertNumQueries(2):
+            response = self.client.get(reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}))
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
 
     def test_list_transcription_entities(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
@@ -1357,7 +1368,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             worker_version=self.worker_version_2
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.get(
                 reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}),
                 {"worker_version": str(self.worker_version_2.id)}
@@ -1397,7 +1408,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             worker_run=self.worker_run_2,
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.get(
                 reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}),
                 {"worker_run": str(self.worker_run_2.id)}
@@ -1438,7 +1449,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             length=len(self.entity.name)
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}),
                 {"worker_version": False}
@@ -1477,7 +1488,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             worker_version=self.worker_version_2,
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.get(
                 reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}),
                 {"entity_worker_version": str(self.worker_version_1.id)}
@@ -1523,7 +1534,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             worker_run=self.worker_run_2,
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.get(
                 reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}),
                 {"entity_worker_run": str(self.worker_run_1.id)}
@@ -1573,7 +1584,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             worker_version=self.worker_version_2
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}),
                 {"entity_worker_version": False}
@@ -1622,7 +1633,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
             worker_version=self.worker_version_1
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.get(
                 reverse("api:transcription-entities", kwargs={"pk": str(self.transcription.id)}),
                 {"entity_worker_version": str(self.worker_version_1.id), "worker_version": str(self.worker_version_2.id)}
@@ -1692,11 +1703,15 @@ class TestEntitiesAPI(FixtureAPITestCase):
             ]
         })
 
-    def test_list_corpus_entities_acl(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_list_corpus_entities_acl(self, has_access_mock):
         with self.assertNumQueries(1):
             response = self.client.get(reverse("api:corpus-entities", kwargs={"pk": str(self.private_corpus.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(AnonymousUser(), self.private_corpus, Role.Guest.value, skip_public=False))
+
     def test_list_corpus_entities_parent(self):
         with self.assertNumQueries(4):
             response = self.client.get(
@@ -1803,7 +1818,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_update_entity_requires_name_and_type(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:entity-details", kwargs={"pk": self.entity_bis.id}),
                 {"name": "a new name"},
@@ -1813,7 +1828,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_update_validate_entity(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.put(
                 reverse("api:entity-details", kwargs={"pk": self.entity_bis.id}),
                 {
@@ -1829,7 +1844,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_update_unvalidate_entity(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.put(
                 reverse("api:entity-details", kwargs={"pk": self.entity_bis.id}),
                 {
@@ -1899,7 +1914,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_update_entity_change_type(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.put(
                 reverse("api:entity-details", kwargs={"pk": self.entity_bis.id}),
                 {
@@ -1918,7 +1933,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_partial_update_validate_entity(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.patch(
                 reverse("api:entity-details", kwargs={"pk": self.entity_bis.id}),
                 {"validated": True},
@@ -1930,7 +1945,7 @@ class TestEntitiesAPI(FixtureAPITestCase):
 
     def test_partial_update_unvalidate_entity(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.patch(
                 reverse("api:entity-details", kwargs={"pk": self.entity_bis.id}),
                 {"validated": False},
@@ -1978,22 +1993,23 @@ class TestEntitiesAPI(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_list_element_links_not_guest(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_list_element_links_not_guest(self, has_access_mock):
         """
         A guest access on the element is required to list entity links
         """
-        private_elt = self.private_corpus.elements.create(
-            type=self.private_corpus.types.create(slug="page"),
-            name="A"
-        )
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
-            response = self.client.get(reverse("api:element-links", kwargs={"pk": str(private_elt.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+        with self.assertNumQueries(3):
+            response = self.client.get(reverse("api:element-links", kwargs={"pk": str(self.element.id)}))
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(
             response.json(),
             {"detail": "You do not have access to this element."}
         )
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.corpus, Role.Guest.value, skip_public=False))
 
     def test_list_element_links(self):
         link = EntityLink.objects.create(parent=self.entity, child=self.entity_bis, role=self.role)
diff --git a/arkindex/documents/tests/test_entity_types.py b/arkindex/documents/tests/test_entity_types.py
index 7bbd833e8f..7f883e4eea 100644
--- a/arkindex/documents/tests/test_entity_types.py
+++ b/arkindex/documents/tests/test_entity_types.py
@@ -1,3 +1,5 @@
+from unittest.mock import call, patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -31,7 +33,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
 
     # ENTITY TYPE CREATE
 
-    def test_create_entity_type_requires_login(self):
+    def test_create_requires_login(self):
         data = {
             "corpus": str(self.corpus.id),
             "name": "dagger one"
@@ -40,7 +42,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             response = self.client.post(reverse("api:entity-type-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_create_entity_type_requires_verified(self):
+    def test_create_requires_verified(self):
         self.client.force_login(self.unverified_user)
         data = {
             "corpus": str(self.corpus.id),
@@ -54,30 +56,36 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             {"detail": "You do not have permission to perform this action."}
         )
 
-    def test_create_entity_type_requires_corpus_admin(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_create_requires_corpus_admin(self, filter_rights_mock):
         self.client.force_login(self.user)
         data = {
             "corpus": str(self.private_corpus.id),
             "name": "dagger one"
         }
         self.assertEqual(self.private_corpus.entity_types.all().count(), 1)
-        with self.assertNumQueries(5):
+
+        with self.assertNumQueries(2):
             response = self.client.post(reverse("api:entity-type-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertDictEqual(
             response.json(),
             {"corpus": [f'Invalid pk "{str(self.private_corpus.id)}" - object does not exist.']}
         )
         self.assertEqual(self.private_corpus.entity_types.all().count(), 1)
 
-    def test_create_entity_type(self):
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Admin.value))
+
+    def test_create(self):
         self.client.force_login(self.private_corpus_admin)
         data = {
             "corpus": str(self.private_corpus.id),
             "name": "dagger spare"
         }
         self.assertEqual(self.private_corpus.entity_types.all().count(), 1)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:entity-type-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(self.private_corpus.entity_types.all().count(), 2)
@@ -85,7 +93,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
         self.assertEqual(response.json()["name"], "dagger spare")
         self.assertEqual(response.json()["corpus"], str(self.private_corpus.id))
 
-    def test_create_entity_type_color_validation(self):
+    def test_create_color_validation(self):
         self.client.force_login(self.private_corpus_admin)
         cases = [
             [
@@ -105,7 +113,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             ]
         ]
         for color, errors in cases:
-            with (self.subTest(color=color, errors=errors), self.assertNumQueries(5)):
+            with (self.subTest(color=color, errors=errors), self.assertNumQueries(3)):
                 response = self.client.post(
                     reverse("api:entity-type-create"),
                     data={
@@ -118,19 +126,19 @@ class TestEntityTypesAPI(FixtureAPITestCase):
                 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
             self.assertDictEqual(response.json(), {"color": errors})
 
-    def test_create_entity_type_color_ignore_case(self):
+    def test_create_color_ignore_case(self):
         self.client.force_login(self.private_corpus_admin)
         data = {
             "corpus": str(self.private_corpus.id),
             "name": "dagger spare",
             "color": "FF0000"
         }
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:entity-type-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(response.json()["color"], "FF0000")
 
-    def test_create_entity_type_color(self):
+    def test_create_color(self):
         self.client.force_login(self.private_corpus_admin)
         data = {
             "corpus": str(self.private_corpus.id),
@@ -138,7 +146,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             "color": "f76719"
         }
         self.assertEqual(self.private_corpus.entity_types.all().count(), 1)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:entity-type-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(self.private_corpus.entity_types.all().count(), 2)
@@ -146,21 +154,21 @@ class TestEntityTypesAPI(FixtureAPITestCase):
         self.assertEqual(response.json()["name"], "dagger spare")
         self.assertEqual(response.json()["corpus"], str(self.private_corpus.id))
 
-    def test_create_entity_type_already_exists(self):
+    def test_create_already_exists(self):
         self.client.force_login(self.private_corpus_admin)
         data = {
             "corpus": str(self.private_corpus.id),
             "name": "some type",
             "color": "f76719"
         }
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:entity-type-create"), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"name": ["Entity Type with name some type already exists in corpus private."]})
 
     # ENTITY TYPE UPDATE
 
-    def test_update_entity_type_requires_login(self):
+    def test_update_requires_login(self):
         data = {
             "name": "dagger one",
             "color": "f76719"
@@ -169,7 +177,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             response = self.client.put(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_update_entity_type_requires_verified(self):
+    def test_update_requires_verified(self):
         self.client.force_login(self.unverified_user)
         data = {
             "name": "dagger one",
@@ -183,15 +191,18 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             {"detail": "You do not have permission to perform this action."}
         )
 
-    def test_update_entity_type_requires_corpus_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_update_requires_corpus_admin(self, has_access_mock):
         self.client.force_login(self.user)
         data = {
             "name": "dagger one",
             "color": "f76719"
         }
-        with self.assertNumQueries(7):
+
+        with self.assertNumQueries(4):
             response = self.client.put(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(
             response.json(),
             {"detail": "You do not have admin access to this corpus."}
@@ -200,27 +211,30 @@ class TestEntityTypesAPI(FixtureAPITestCase):
         self.assertEqual(self.private_corpus_entity_type.name, "some type")
         self.assertEqual(self.private_corpus_entity_type.color, "ff0000")
 
-    def test_update_entity_type(self):
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Admin.value, skip_public=False))
+
+    def test_update(self):
         self.client.force_login(self.private_corpus_admin)
         data = {
             "name": "dagger spare",
             "color": "f76719"
         }
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.put(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.private_corpus_entity_type.refresh_from_db()
         self.assertEqual(self.private_corpus_entity_type.color, "f76719")
         self.assertEqual(self.private_corpus_entity_type.name, "dagger spare")
 
-    def test_update_entity_type_name_already_exists(self):
+    def test_update_name_already_exists(self):
         self.client.force_login(self.private_corpus_admin)
         EntityType.objects.create(corpus=self.private_corpus, name="dagger spare")
         data = {
             "name": "dagger spare",
             "color": "f76719"
         }
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.put(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"name": ["Entity Type with name dagger spare already exists in corpus private."]})
@@ -230,7 +244,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
         data = {
             "color": "f76719"
         }
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"name": ["This field is required."]})
@@ -241,14 +255,14 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             "corpus": str(self.corpus.id),
             "name": "iceman"
         }
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"corpus": ["It is not possible to update an Entity Type's corpus."]})
 
     # ENTITY TYPE PARTIAL UPDATE
 
-    def test_partial_update_entity_type_requires_login(self):
+    def test_partial_update_requires_login(self):
         data = {
             "name": "dagger one",
             "color": "f76719"
@@ -257,7 +271,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             response = self.client.patch(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_partial_update_entity_type_requires_verified(self):
+    def test_partial_update_requires_verified(self):
         self.client.force_login(self.unverified_user)
         data = {
             "name": "dagger one",
@@ -271,15 +285,18 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             {"detail": "You do not have permission to perform this action."}
         )
 
-    def test_partial_update_entity_type_requires_corpus_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_partial_update_requires_corpus_admin(self, has_access_mock):
         self.client.force_login(self.user)
         data = {
             "name": "dagger one",
             "color": "f76719"
         }
-        with self.assertNumQueries(7):
+
+        with self.assertNumQueries(4):
             response = self.client.patch(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(
             response.json(),
             {"detail": "You do not have admin access to this corpus."}
@@ -288,13 +305,16 @@ class TestEntityTypesAPI(FixtureAPITestCase):
         self.assertEqual(self.private_corpus_entity_type.name, "some type")
         self.assertEqual(self.private_corpus_entity_type.color, "ff0000")
 
-    def test_partial_update_entity_type(self):
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Admin.value, skip_public=False))
+
+    def test_partial_update(self):
         self.client.force_login(self.private_corpus_admin)
         data = {
             "name": "dagger spare",
             "color": "f76719"
         }
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.patch(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json()["color"], "f76719")
@@ -305,7 +325,7 @@ class TestEntityTypesAPI(FixtureAPITestCase):
         data = {
             "color": "f76719"
         }
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.patch(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.private_corpus_entity_type.refresh_from_db()
@@ -317,14 +337,14 @@ class TestEntityTypesAPI(FixtureAPITestCase):
         data = {
             "corpus": str(self.corpus.id),
         }
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.patch(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), data=data, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"corpus": ["It is not possible to update an Entity Type's corpus."]})
 
     # ENTITY TYPE RETRIEVE
 
-    def test_retrieve_entity_type_does_not_require_login_public_project(self):
+    def test_retrieve_does_not_require_login_public_project(self):
         with self.assertNumQueries(1):
             response = self.client.get(reverse("api:entity-type-details", kwargs={"pk": self.location_type.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -335,19 +355,25 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             "color": "ff0000"
         })
 
-    def test_retrieve_entity_type_requires_private_corpus_guest(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_retrieve_requires_private_corpus_guest(self, filter_rights_mock):
         self.client.force_login(self.no_access)
-        with self.assertNumQueries(5):
+
+        with self.assertNumQueries(2):
             response = self.client.get(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
         self.assertDictEqual(
             response.json(),
             {"detail": "Not found."}
         )
 
-    def test_retrieve_entity_type(self):
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.no_access, Corpus, Role.Guest.value))
+
+    def test_retrieve(self):
         self.client.force_login(self.private_corpus_guest)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json()["color"], "ff0000")
@@ -355,9 +381,10 @@ class TestEntityTypesAPI(FixtureAPITestCase):
 
     # ENTITY TYPE DELETE
 
-    def test_delete_entity_type_requires_corpus_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_delete_requires_corpus_admin(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(
@@ -365,23 +392,26 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             {"detail": "You do not have admin access to this corpus."}
         )
 
-    def test_delete_entity_type(self):
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Admin.value, skip_public=False))
+
+    def test_delete(self):
         self.client.force_login(self.private_corpus_admin)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.delete(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         with self.assertRaises(EntityType.DoesNotExist):
             self.private_corpus_entity_type.refresh_from_db()
 
-    def test_delete_entity_type_has_entities(self):
+    def test_delete_has_entities(self):
         Entity.objects.create(type=self.private_corpus_entity_type, corpus=self.private_corpus, name="bob")
         self.client.force_login(self.private_corpus_admin)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.delete(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"detail": ["Some entities are using this entity type."]})
 
-    def test_delete_entity_type_has_roles(self):
+    def test_delete_has_roles(self):
         EntityRole.objects.create(
             corpus=self.private_corpus,
             parent_name="goose",
@@ -390,14 +420,14 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             child_type=self.private_corpus_entity_type
         )
         self.client.force_login(self.private_corpus_admin)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(reverse("api:entity-type-details", kwargs={"pk": self.private_corpus_entity_type.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"detail": ["Some entity roles are using this entity type."]})
 
     # LIST CORPUS ENTITY TYPES
 
-    def test_corpus_entity_types_does_not_require_login_public_project(self):
+    def test_list_does_not_require_login_public_project(self):
         with self.assertNumQueries(3):
             response = self.client.get(reverse("api:corpus-entity-types", kwargs={"pk": self.corpus.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -429,17 +459,21 @@ class TestEntityTypesAPI(FixtureAPITestCase):
             },
         ])
 
-    def test_corpus_entity_types_requires_guest_private_project(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_list_requires_guest_private_project(self, has_access_mock):
         self.client.force_login(self.no_access)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:corpus-entity-types", kwargs={"pk": self.private_corpus.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_corpus_entity_types(self):
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.no_access, self.private_corpus, Role.Guest.value, skip_public=False))
+
+    def test_list(self):
         type_2 = EntityType.objects.create(corpus=self.private_corpus, color="f01ef7", name="type 2")
         type_3 = EntityType.objects.create(corpus=self.private_corpus, color="29d911", name="person")
         self.client.force_login(self.private_corpus_guest)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:corpus-entity-types", kwargs={"pk": self.private_corpus.id}), format="json")
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertCountEqual(response.json()["results"], [
diff --git a/arkindex/documents/tests/test_export.py b/arkindex/documents/tests/test_export.py
index 8016de5ee6..d1ab1e81d1 100644
--- a/arkindex/documents/tests/test_export.py
+++ b/arkindex/documents/tests/test_export.py
@@ -61,13 +61,16 @@ class TestExport(FixtureAPITestCase):
         self.assertFalse(delay_mock.called)
 
     @patch("arkindex.project.triggers.export.export_corpus.delay")
-    def test_start_requires_contributor(self, delay_mock):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_start_requires_contributor(self, has_access_mock, delay_mock):
         self.user.rights.update(level=Role.Guest.value)
         self.client.force_login(self.user)
 
         response = self.client.post(reverse("api:corpus-export", kwargs={"pk": self.corpus.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.corpus, Role.Contributor.value, skip_public=False))
         self.assertFalse(self.corpus.exports.exists())
         self.assertFalse(delay_mock.called)
 
@@ -146,14 +149,19 @@ class TestExport(FixtureAPITestCase):
         response = self.client.get(reverse("api:corpus-export", kwargs={"pk": self.corpus.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_list_requires_guest(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_list_requires_guest(self, has_access_mock):
         self.user.rights.all().delete()
         self.corpus.public = False
         self.corpus.save()
         self.client.force_login(self.user)
+
         response = self.client.get(reverse("api:corpus-export", kwargs={"pk": self.corpus.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.corpus, Role.Guest.value, skip_public=False))
+
     @patch("arkindex.project.aws.s3.meta.client.generate_presigned_url")
     def test_download_export(self, presigned_url_mock):
         presigned_url_mock.return_value = "http://somewhere"
@@ -182,7 +190,7 @@ class TestExport(FixtureAPITestCase):
         self.corpus.memberships.filter(user=self.user).delete()
         export = self.corpus.exports.create(user=self.superuser, state=CorpusExportState.Done)
 
-        with self.assertNumQueries(4):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:manage-export", kwargs={"pk": export.id}))
             self.assertEqual(response.status_code, status.HTTP_302_FOUND)
         self.assertEqual(response.headers["Location"], "http://somewhere")
@@ -201,16 +209,20 @@ class TestExport(FixtureAPITestCase):
             response = self.client.get(reverse("api:manage-export", kwargs={"pk": export.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_download_export_requires_guest(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_download_export_requires_guest(self, filter_rights_mock):
         self.user.rights.all().delete()
         self.corpus.public = False
         self.corpus.save()
         self.client.force_login(self.user)
         export = self.corpus.exports.create(user=self.user, state=CorpusExportState.Done)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.get(reverse("api:manage-export", kwargs={"pk": export.id}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
     def test_download_export_not_done(self):
         self.client.force_login(self.superuser)
         for state in (CorpusExportState.Created, CorpusExportState.Running, CorpusExportState.Failed):
@@ -236,14 +248,18 @@ class TestExport(FixtureAPITestCase):
             response = self.client.delete(reverse("api:manage-export", kwargs={"pk": export.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_delete_export_private_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_delete_export_private_corpus(self, filter_rights_mock):
         private_corpus = Corpus.objects.create(name="private")
         self.client.force_login(self.user)
         export = private_corpus.exports.create(user=self.superuser, state=CorpusExportState.Done)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.delete(reverse("api:manage-export", kwargs={"pk": export.id}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
     def test_delete_export_wrong_state(self):
         self.client.force_login(self.superuser)
         for state in (CorpusExportState.Created, CorpusExportState.Failed):
@@ -277,28 +293,52 @@ class TestExport(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         assert not self.corpus.exports.exists()
 
-    def test_delete_export_requires_rights(self):
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Contributor.value)
+    def test_delete_export_requires_admin(self, get_max_level_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
         self.client.force_login(self.user)
         export = self.corpus.exports.create(user=self.superuser, state=CorpusExportState.Done)
-        with self.assertNumQueries(5):
+
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:manage-export", kwargs={"pk": export.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have sufficient rights to delete this export."})
 
-    def test_delete_export_creator_contributor(self):
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.corpus))
+
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Guest.value)
+    def test_delete_export_creator_requires_contributor(self, get_max_level_mock):
+        self.client.force_login(self.user)
+        export = self.corpus.exports.create(user=self.user, state=CorpusExportState.Done)
+
+        with self.assertNumQueries(3):
+            response = self.client.delete(reverse("api:manage-export", kwargs={"pk": export.id}))
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+        self.assertDictEqual(response.json(), {"detail": "You do not have sufficient rights to delete this export."})
+
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.corpus))
+
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Contributor.value)
+    def test_delete_export_creator_contributor(self, get_max_level_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
         self.client.force_login(self.user)
         export = self.corpus.exports.create(user=self.user, state=CorpusExportState.Done)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:manage-export", kwargs={"pk": export.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         assert not self.corpus.exports.exists()
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.corpus))
+
     def test_delete_export_corpus_admin(self):
         self.client.force_login(self.user)
         export = self.corpus.exports.create(user=self.superuser, state=CorpusExportState.Done)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:manage-export", kwargs={"pk": export.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         assert not self.corpus.exports.exists()
diff --git a/arkindex/documents/tests/test_metadata.py b/arkindex/documents/tests/test_metadata.py
index a9a0ebe324..6968b3cbbf 100644
--- a/arkindex/documents/tests/test_metadata.py
+++ b/arkindex/documents/tests/test_metadata.py
@@ -1,5 +1,7 @@
 import uuid
+from unittest.mock import call, patch
 
+from django.contrib.auth.models import AnonymousUser
 from django.urls import reverse
 from rest_framework import status
 
@@ -54,7 +56,7 @@ class TestMetaData(FixtureAPITestCase):
 
     def test_list(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertListEqual(response.json(), [
@@ -70,27 +72,14 @@ class TestMetaData(FixtureAPITestCase):
             },
         ])
 
-    def test_list_public(self):
-        self.assertTrue(self.corpus.public)
-        with self.assertNumQueries(2):
-            response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertListEqual(response.json(), [
-            {
-                "id": str(self.metadata.id),
-                "name": "folio",
-                "type": "text",
-                "value": "123",
-                "dates": [],
-                "entity": None,
-                "worker_version": None,
-                "worker_run": None,
-            },
-        ])
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_list_private_requires_login(self, filter_rights_mock):
+        with self.assertNumQueries(0):
+            response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.private_vol.id)}))
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_list_private_requires_login(self):
-        response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.private_vol.id)}))
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(AnonymousUser(), Corpus, Role.Guest.value))
 
     def test_list_with_entity(self):
         # Link an entity to metadata, db requests count should remains same
@@ -106,7 +95,7 @@ class TestMetaData(FixtureAPITestCase):
             entity=entity,
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertCountEqual(response.json(), [
@@ -148,7 +137,7 @@ class TestMetaData(FixtureAPITestCase):
         self.metadata.worker_version = self.worker_version
         self.metadata.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertListEqual(response.json(), [
@@ -170,7 +159,7 @@ class TestMetaData(FixtureAPITestCase):
         self.metadata.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -190,22 +179,26 @@ class TestMetaData(FixtureAPITestCase):
             }
         ])
 
-    def test_list_wrong_acl(self):
-        self.vol.corpus = self.private_corpus
-        self.vol.save()
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_list_wrong_acl(self, filter_rights_mock):
         self.client.force_login(self.user)
-        response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}))
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        with self.assertNumQueries(2):
+            response = self.client.get(reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}))
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
 
     def test_list_wrong_element_id(self):
         self.client.force_login(self.user)
-        response = self.client.get(reverse("api:element-metadata", kwargs={"pk": "12341234-1234-1234-1234-123412341234"}))
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        with self.assertNumQueries(3):
+            response = self.client.get(reverse("api:element-metadata", kwargs={"pk": "12341234-1234-1234-1234-123412341234"}))
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
     def test_list_without_parents(self):
         self.vol.metadatas.create(type=MetaType.Numeric, name="weight", value="1337")
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:element-metadata", kwargs={"pk": str(self.page.id)}),
                 {"load_parents": False},
@@ -230,7 +223,7 @@ class TestMetaData(FixtureAPITestCase):
         weight_meta = self.vol.metadatas.create(type=MetaType.Numeric, name="weight", value="1337.0")
         self.client.force_login(self.user)
         # One more request is required to list element paths
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:element-metadata", kwargs={"pk": str(self.page.id)}),
                 {"load_parents": True},
@@ -298,17 +291,23 @@ class TestMetaData(FixtureAPITestCase):
             response = method(reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}))
             self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
 
-    def test_create_metadata_writable_element(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_create_metadata_writable_element(self, filter_rights_mock):
         self.client.force_login(self.user)
         hidden_book = self.private_corpus.elements.create(
             type=self.corpus.types.get(slug="volume"),
             name="Nope"
         )
-        response = self.client.post(
-            reverse("api:element-metadata", kwargs={"pk": str(hidden_book.id)}),
-            data={"type": "text", "name": "language", "value": "Japanese"}
-        )
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        with self.assertNumQueries(2):
+            response = self.client.post(
+                reverse("api:element-metadata", kwargs={"pk": str(hidden_book.id)}),
+                data={"type": "text", "name": "language", "value": "Japanese"}
+            )
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Contributor.value))
 
     def test_create_metadata(self):
         self.client.force_login(self.user)
@@ -322,7 +321,7 @@ class TestMetaData(FixtureAPITestCase):
 
     def test_create_metadata_worker_version(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}),
                 data={
@@ -339,7 +338,7 @@ class TestMetaData(FixtureAPITestCase):
 
     def test_create_metadata_worker_run_or_version(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}),
                 data={
@@ -418,7 +417,7 @@ class TestMetaData(FixtureAPITestCase):
 
     def test_create_duplicated_metadata(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}),
                 data={"type": "date", "name": "edition", "value": "1986-june"}
@@ -504,7 +503,7 @@ class TestMetaData(FixtureAPITestCase):
         other_worker_run = process2.worker_runs.create(version=self.worker_run.version, parents=[])
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}),
                 data={"type": "text", "name": "color", "value": "red", "worker_run_id": str(other_worker_run.id)},
@@ -522,7 +521,7 @@ class TestMetaData(FixtureAPITestCase):
         A regular user can create a metadata with a WorkerRun of their own local process
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}),
                 data={"type": "location", "name": "location", "value": "Texas", "worker_run_id": str(self.local_worker_run.id)}
@@ -554,7 +553,7 @@ class TestMetaData(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}),
                 data={"type": "text", "name": "color", "value": "red", "worker_run_id": str(self.worker_run.id)},
@@ -578,7 +577,7 @@ class TestMetaData(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:element-metadata", kwargs={"pk": str(self.vol.id)}),
                 data={"type": "text", "name": "color", "value": "red", "worker_run_id": str(local_worker_run.id)},
@@ -629,20 +628,27 @@ class TestMetaData(FixtureAPITestCase):
         )
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
-    def test_delete_metadata_private_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_delete_metadata_private_corpus(self, filter_rights_mock):
         self.client.force_login(self.user)
-        response = self.client.delete(reverse("api:metadata-edit", kwargs={"pk": str(self.private_metadata.id)}))
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        with self.assertNumQueries(2):
+            response = self.client.delete(reverse("api:metadata-edit", kwargs={"pk": str(self.private_metadata.id)}))
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_delete_metadata_readable_element(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_delete_metadata_readable_element(self, has_access_mock):
         """
         An explicit message should be raised when user can read but not delete metadata
         """
         self.client.force_login(self.user)
-        self.private_corpus.memberships.create(user=self.user, level=Role.Guest.value)
-        response = self.client.delete(reverse("api:metadata-edit", kwargs={"pk": str(self.private_metadata.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+        with self.assertNumQueries(3):
+            response = self.client.delete(reverse("api:metadata-edit", kwargs={"pk": str(self.private_metadata.id)}))
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have write access to this corpus."})
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Contributor.value, skip_public=False))
 
     def test_admin_create_any(self):
         """
@@ -993,7 +999,7 @@ class TestMetaData(FixtureAPITestCase):
     def test_get_metadata(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:metadata-edit", kwargs={"pk": str(self.metadata.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -1019,7 +1025,7 @@ class TestMetaData(FixtureAPITestCase):
         self.metadata.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:metadata-edit", kwargs={"pk": str(self.metadata.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -1043,7 +1049,7 @@ class TestMetaData(FixtureAPITestCase):
         self.metadata.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:metadata-edit", kwargs={"pk": str(self.metadata.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -1305,21 +1311,27 @@ class TestMetaData(FixtureAPITestCase):
             response = method(reverse("api:element-metadata-bulk", kwargs={"pk": str(self.vol.id)}))
             self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
 
-    def test_bulk_create_metadata_writable_element(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_bulk_create_metadata_writable_element(self, filter_rights_mock):
         self.client.force_login(self.user)
         hidden_book = self.private_corpus.elements.create(
             type=self.corpus.types.get(slug="volume"),
             name="Nope"
         )
-        response = self.client.post(
-            reverse("api:element-metadata-bulk", kwargs={"pk": str(hidden_book.id)}),
-            data={
-                "worker_run_id": str(self.worker_run.id),
-                "metadata_list": [{"type": "text", "name": "language", "value": "Japanese"}],
-            },
-            format="json"
-        )
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        with self.assertNumQueries(2):
+            response = self.client.post(
+                reverse("api:element-metadata-bulk", kwargs={"pk": str(hidden_book.id)}),
+                data={
+                    "worker_run_id": str(self.worker_run.id),
+                    "metadata_list": [{"type": "text", "name": "language", "value": "Japanese"}],
+                },
+                format="json"
+            )
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Contributor.value))
 
     def test_bulk_create_metadata_local(self):
         """
@@ -1328,7 +1340,7 @@ class TestMetaData(FixtureAPITestCase):
         entity = self.corpus.entities.create(name="42", type=self.number_type)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:element-metadata-bulk", kwargs={"pk": str(self.vol.id)}),
                 data={
@@ -1389,7 +1401,7 @@ class TestMetaData(FixtureAPITestCase):
         Worker run must be specified, worker version is forbidden
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:element-metadata-bulk", kwargs={"pk": str(self.vol.id)}),
                 data={"metadata_list": [
@@ -1503,7 +1515,7 @@ class TestMetaData(FixtureAPITestCase):
         self.worker_run.process.run()
         task = self.worker_run.process.tasks.first()
 
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:element-metadata-bulk", kwargs={"pk": str(self.vol.id)}),
                 data={
@@ -1519,7 +1531,7 @@ class TestMetaData(FixtureAPITestCase):
 
     def test_bulk_create_metadata_malicious_data(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:element-metadata-bulk", kwargs={"pk": str(self.vol.id)}),
                 data={"metadata_list": [{
@@ -1602,7 +1614,7 @@ class TestMetaData(FixtureAPITestCase):
         """
         self.vol.metadatas.all().delete()
         process_worker_run = self.process.worker_runs.get()
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:element-metadata-bulk", kwargs={"pk": str(self.vol.id)}),
                 data={
@@ -1657,7 +1669,7 @@ class TestMetaData(FixtureAPITestCase):
         The metadata created with the bulk endpoint must be unique
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:element-metadata-bulk", kwargs={"pk": str(self.vol.id)}),
                 data={
@@ -1709,7 +1721,7 @@ class TestMetaData(FixtureAPITestCase):
         local_worker_run = local_process.worker_runs.get()
         self.vol.metadatas.all().delete()
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:element-metadata-bulk", kwargs={"pk": str(self.vol.id)}),
                 data={
diff --git a/arkindex/documents/tests/test_move_element.py b/arkindex/documents/tests/test_move_element.py
index 9380575afc..d667bd311f 100644
--- a/arkindex/documents/tests/test_move_element.py
+++ b/arkindex/documents/tests/test_move_element.py
@@ -29,14 +29,15 @@ class TestMoveElement(FixtureAPITestCase):
             response = self.client.post(reverse("api:move-element"), {"source": str(self.source.id), "destination": str(self.destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_move_element_wrong_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_move_element_wrong_acl(self, filter_rights_mock):
         private_corpus = Corpus.objects.create(name="private", public=False)
         private_element = private_corpus.elements.create(
             type=private_corpus.types.create(slug="folder"),
         )
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(2):
             response = self.client.post(reverse("api:move-element"), {"source": str(private_element.id), "destination": str(private_element.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -49,7 +50,7 @@ class TestMoveElement(FixtureAPITestCase):
 
     def test_move_element_wrong_source(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:move-element"), {"source": "12341234-1234-1234-1234-123412341234", "destination": str(self.destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -59,7 +60,7 @@ class TestMoveElement(FixtureAPITestCase):
 
     def test_move_element_wrong_destination(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:move-element"), {"source": str(self.source.id), "destination": "12341234-1234-1234-1234-123412341234"}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -69,7 +70,7 @@ class TestMoveElement(FixtureAPITestCase):
 
     def test_move_element_same_source_destination(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:move-element"), {"source": str(self.source.id), "destination": str(self.source.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -83,7 +84,7 @@ class TestMoveElement(FixtureAPITestCase):
         destination = corpus2.elements.create(type=corpus2.types.create(slug="folder"))
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:move-element"), {"source": str(self.source.id), "destination": str(destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -95,7 +96,7 @@ class TestMoveElement(FixtureAPITestCase):
         destination = self.corpus.elements.get(name="Volume 1")
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:move-element"), {"source": str(self.source.id), "destination": str(destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -108,7 +109,7 @@ class TestMoveElement(FixtureAPITestCase):
         destination_id = self.source.id
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(reverse("api:move-element"), {"source": str(source.id), "destination": str(destination_id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -119,7 +120,7 @@ class TestMoveElement(FixtureAPITestCase):
     @patch("arkindex.project.triggers.documents_tasks.move_element.delay")
     def test_move_element(self, delay_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(reverse("api:move-element"), {"source": str(self.source.id), "destination": str(self.destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
diff --git a/arkindex/documents/tests/test_move_selection.py b/arkindex/documents/tests/test_move_selection.py
index 956449c7d5..d524373efe 100644
--- a/arkindex/documents/tests/test_move_selection.py
+++ b/arkindex/documents/tests/test_move_selection.py
@@ -35,7 +35,7 @@ class TestMoveSelection(FixtureAPITestCase):
     @override_settings(ARKINDEX_FEATURES={"selection": False})
     def test_move_selection_disabled(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:move-selection"), {"corpus_id": str(self.corpus.id), "destination": str(self.destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
             self.assertDictEqual(
@@ -43,14 +43,15 @@ class TestMoveSelection(FixtureAPITestCase):
             )
 
     @override_settings(ARKINDEX_FEATURES={"selection": True})
-    def test_move_selection_wrong_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_move_selection_wrong_acl(self, filter_rights_mock):
         private_corpus = Corpus.objects.create(name="private", public=False)
         private_element = private_corpus.elements.create(
             type=private_corpus.types.create(slug="folder"),
         )
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(2):
             response = self.client.post(reverse("api:move-selection"), {"corpus_id": str(private_corpus.id), "destination": str(private_element.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -65,7 +66,7 @@ class TestMoveSelection(FixtureAPITestCase):
     def test_move_selection_wrong_destination(self):
         self.client.force_login(self.user)
         self.user.selected_elements.add(self.page)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:move-selection"), {"corpus_id": str(self.corpus.id), "destination": "12341234-1234-1234-1234-123412341234"}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -77,7 +78,7 @@ class TestMoveSelection(FixtureAPITestCase):
     def test_move_selection_same_destination(self):
         self.client.force_login(self.user)
         self.user.selected_elements.add(self.page)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.post(reverse("api:move-selection"), {"corpus_id": str(self.corpus.id), "destination": str(self.page.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -92,7 +93,7 @@ class TestMoveSelection(FixtureAPITestCase):
         destination = corpus2.elements.create(type=corpus2.types.create(slug="folder"))
         self.client.force_login(self.user)
         self.user.selected_elements.add(self.page)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(5):
             response = self.client.post(reverse("api:move-selection"), {"corpus_id": str(self.corpus.id), "destination": str(destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -105,7 +106,7 @@ class TestMoveSelection(FixtureAPITestCase):
         destination = self.corpus.elements.get(name="Volume 1")
         self.client.force_login(self.user)
         self.user.selected_elements.add(self.page)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(reverse("api:move-selection"), {"corpus_id": str(self.corpus.id), "destination": str(destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -119,7 +120,7 @@ class TestMoveSelection(FixtureAPITestCase):
         destination_id = self.page.id
         self.client.force_login(self.user)
         self.user.selected_elements.add(target)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.post(reverse("api:move-selection"), {"corpus_id": str(self.corpus.id), "destination": str(destination_id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -134,7 +135,7 @@ class TestMoveSelection(FixtureAPITestCase):
         self.user.selected_elements.add(self.page)
         another_page = self.corpus.elements.get(name="Volume 1, page 1v")
         self.user.selected_elements.add(another_page)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.post(reverse("api:move-selection"), {"corpus_id": str(self.corpus.id), "destination": str(self.destination.id)}, format="json")
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
diff --git a/arkindex/documents/tests/test_neighbors.py b/arkindex/documents/tests/test_neighbors.py
index 0268555882..01340642c3 100644
--- a/arkindex/documents/tests/test_neighbors.py
+++ b/arkindex/documents/tests/test_neighbors.py
@@ -1,3 +1,5 @@
+from unittest.mock import call, patch
+
 from django.db import transaction
 from django.urls import reverse
 from rest_framework import status
@@ -5,6 +7,7 @@ from rest_framework import status
 from arkindex.documents.models import Corpus, Element
 from arkindex.project.tests import FixtureAPITestCase
 from arkindex.project.tools import build_tree
+from arkindex.users.models import Role
 
 
 class TestElementNeighbors(FixtureAPITestCase):
@@ -15,7 +18,8 @@ class TestElementNeighbors(FixtureAPITestCase):
         cls.volume_type = cls.corpus.types.get(slug="volume")
         cls.page_type = cls.corpus.types.get(slug="page")
 
-    def test_element_neighbors_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_element_neighbors_acl(self, filter_rights_mock):
         """
         A Guest access is required to list neighbors of an element
         """
@@ -31,10 +35,13 @@ class TestElementNeighbors(FixtureAPITestCase):
             type=private_type,
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(2):
             response = self.client.get(reverse("api:elements-neighbors", kwargs={"pk": str(elements["A"].id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {"detail": "You do not have a read access to this element."})
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
 
     def test_element_neighbors(self):
         r"""
diff --git a/arkindex/documents/tests/test_patch_elements.py b/arkindex/documents/tests/test_patch_elements.py
index be3da68ab6..4a0bd1c6db 100644
--- a/arkindex/documents/tests/test_patch_elements.py
+++ b/arkindex/documents/tests/test_patch_elements.py
@@ -1,3 +1,5 @@
+from unittest.mock import call, patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -55,7 +57,8 @@ class TestPatchElements(FixtureAPITestCase):
             {"detail": "You do not have permission to perform this action."}
         )
 
-    def test_patch_no_write_access(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_patch_no_write_access(self, has_access_mock):
         # Create read_only corpus right
         self.private_corpus.memberships.create(user=self.user, level=Role.Guest.value)
         self.assertTrue(self.user.verified_email)
@@ -71,14 +74,15 @@ class TestPatchElements(FixtureAPITestCase):
             {"detail": "You do not have write access to this element."}
         )
 
-    def test_patch_element_no_read_access(self):
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Contributor.value, skip_public=False))
+
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_patch_element_no_read_access(self, filter_rights_mock):
         """
         Check patching an element as anonymous user is not possible
         """
-        ext_user = User.objects.create_user(email="ark@ark.net")
-        ext_user.verified_email = True
-        ext_user.save()
-        self.client.force_login(ext_user)
+        self.client.force_login(self.user)
         response = self.client.patch(
             reverse("api:element-retrieve", kwargs={"pk": str(self.private_elt.id)}),
             data={"name": "Untitled (2)"},
@@ -86,9 +90,12 @@ class TestPatchElements(FixtureAPITestCase):
         )
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
     def test_patch_element(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:element-retrieve", kwargs={"pk": str(self.vol.id)}),
                 data={"name": "Untitled (2)"},
diff --git a/arkindex/documents/tests/test_put_elements.py b/arkindex/documents/tests/test_put_elements.py
index 1fac8693c4..c4d42407cb 100644
--- a/arkindex/documents/tests/test_put_elements.py
+++ b/arkindex/documents/tests/test_put_elements.py
@@ -1,3 +1,5 @@
+from unittest.mock import call, patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -9,7 +11,7 @@ from arkindex.project.tests import FixtureAPITestCase
 from arkindex.users.models import Role, User
 
 
-class TestPatchElements(FixtureAPITestCase):
+class TestPutElements(FixtureAPITestCase):
 
     @classmethod
     def setUpTestData(cls):
@@ -48,9 +50,8 @@ class TestPatchElements(FixtureAPITestCase):
             {"detail": "You do not have permission to perform this action."}
         )
 
-    def test_put_no_write_access(self):
-        # Create read_only corpus right
-        self.private_corpus.memberships.create(user=self.user, level=Role.Guest.value)
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_put_no_write_access(self, has_access_mock):
         self.assertTrue(self.user.verified_email)
         self.client.force_login(self.user)
         response = self.client.put(
@@ -63,15 +64,15 @@ class TestPatchElements(FixtureAPITestCase):
             response.json(),
             {"detail": "You do not have write access to this element."}
         )
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Contributor.value, skip_public=False))
 
-    def test_put_element_no_read_access(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_put_element_no_read_access(self, filter_rights_mock):
         """
         Check putting an element as anonymous user is not possible
         """
-        ext_user = User.objects.create_user(email="ark@ark.net")
-        ext_user.verified_email = True
-        ext_user.save()
-        self.client.force_login(ext_user)
+        self.client.force_login(self.user)
         response = self.client.put(
             reverse("api:element-retrieve", kwargs={"pk": str(self.private_elt.id)}),
             data={"name": "Untitled (2)"},
@@ -79,11 +80,14 @@ class TestPatchElements(FixtureAPITestCase):
         )
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
     def test_put_element(self):
         self.client.force_login(self.user)
         self.assertEqual(self.vol.name, "Volume 1")
         self.assertEqual(self.vol.type, self.volume_type)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(7):
             response = self.client.put(
                 reverse("api:element-retrieve", kwargs={"pk": str(self.vol.id)}),
                 data={"name": "Untitled (2)", "type": "text_line"},
diff --git a/arkindex/documents/tests/test_retrieve_elements.py b/arkindex/documents/tests/test_retrieve_elements.py
index 326d1134f2..c8062471c5 100644
--- a/arkindex/documents/tests/test_retrieve_elements.py
+++ b/arkindex/documents/tests/test_retrieve_elements.py
@@ -1,3 +1,5 @@
+from unittest.mock import patch
+
 from django.test import override_settings
 from django.urls import reverse
 from rest_framework import status
@@ -49,7 +51,7 @@ class TestRetrieveElements(FixtureAPITestCase):
             "mirrored": False,
             "created": "2020-02-02T01:23:45.678000Z",
             "creator": None,
-            "rights": ["read"],
+            "rights": ["read", "write", "admin"],
             "metadata_count": 0,
             "classifications": [
                 {
@@ -175,7 +177,8 @@ class TestRetrieveElements(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json()["creator"], "Test user")
 
-    def test_get_element_rights(self):
+    @patch("arkindex.documents.serializers.elements.get_max_level")
+    def test_get_element_rights(self, get_max_level_mock):
         cases = [
             (Role.Guest, self.superuser, ["read"]),
             (Role.Guest, self.user, ["read"]),
@@ -187,12 +190,15 @@ class TestRetrieveElements(FixtureAPITestCase):
         self.client.force_login(self.user)
         for role, creator, expected_rights in cases:
             with self.subTest(role=role, creator=creator):
+                get_max_level_mock.return_value = role.value
                 self.corpus.memberships.filter(user=self.user).update(level=role.value)
                 self.vol.creator = creator
                 self.vol.save()
-                with self.assertNumQueries(6):
+
+                with self.assertNumQueries(4):
                     response = self.client.get(reverse("api:element-retrieve", kwargs={"pk": str(self.vol.id)}))
                     self.assertEqual(response.status_code, status.HTTP_200_OK)
+
                 self.assertListEqual(response.json()["rights"], expected_rights)
 
     def test_get_element_created_by_worker_run(self):
@@ -222,7 +228,7 @@ class TestRetrieveElements(FixtureAPITestCase):
             "mirrored": False,
             "created": "2020-02-02T01:23:45.678000Z",
             "creator": None,
-            "rights": ["read"],
+            "rights": ["read", "write", "admin"],
             "metadata_count": 0,
             "classifications": [],
             "worker_run": {
@@ -257,7 +263,7 @@ class TestRetrieveElements(FixtureAPITestCase):
             "mirrored": False,
             "created": "2020-02-02T01:23:45.678000Z",
             "creator": None,
-            "rights": ["read"],
+            "rights": ["read", "write", "admin"],
             "metadata_count": 0,
             "classifications": [
                 {
diff --git a/arkindex/documents/tests/test_search_api.py b/arkindex/documents/tests/test_search_api.py
index 53ac2d7499..2fe56cd2fe 100644
--- a/arkindex/documents/tests/test_search_api.py
+++ b/arkindex/documents/tests/test_search_api.py
@@ -1,5 +1,6 @@
 from unittest.mock import call, patch
 
+from django.contrib.auth.models import AnonymousUser
 from django.test import override_settings
 from django.urls import reverse
 from rest_framework import status
@@ -70,10 +71,13 @@ class TestSearchApi(FixtureAPITestCase):
         self.assertEqual(response.json(), {"detail": "Not found."})
 
     @override_settings(ARKINDEX_FEATURES={"search": True})
-    def test_corpus_no_permission(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_corpus_no_permission(self, has_access_mock):
         private_corpus = Corpus.objects.create(name="private", public=False)
         response = self.client.get(reverse("api:corpus-search", kwargs={"pk": private_corpus.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(AnonymousUser(), private_corpus, Role.Guest.value, skip_public=False))
 
     @override_settings(ARKINDEX_FEATURES={"search": True})
     def test_corpus_not_indexable(self):
@@ -331,13 +335,17 @@ class TestSearchApi(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
     @override_settings(ARKINDEX_FEATURES={"search": True})
-    def test_build_index_requires_corpus_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_build_index_requires_corpus_admin(self, has_access_mock):
         self.corpus.memberships.update(level=Role.Contributor.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:build-search-index", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.corpus, Role.Admin.value, skip_public=False))
+
     @override_settings(ARKINDEX_FEATURES={"search": False})
     @patch("arkindex.documents.tasks.reindex_corpus.delay")
     def test_build_index_requires_search_feature(self, reindex_trigger_mock):
@@ -358,7 +366,7 @@ class TestSearchApi(FixtureAPITestCase):
         existing_job.ended_at = None
         job_mock.fetch.return_value = existing_job
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:build-search-index", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -373,7 +381,7 @@ class TestSearchApi(FixtureAPITestCase):
         self.corpus.indexable = False
         self.corpus.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:build-search-index", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -387,7 +395,7 @@ class TestSearchApi(FixtureAPITestCase):
     def test_build_index_non_indexable_element_types(self, job_mock, reindex_trigger_mock):
         self.corpus.types.update(indexable=False)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:build-search-index", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -404,7 +412,7 @@ class TestSearchApi(FixtureAPITestCase):
         existing_job.ended_at = "2000-01-01"
         job_mock.fetch.return_value = existing_job
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:build-search-index", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertDictEqual(response.json(), {"drop": True})
@@ -424,7 +432,7 @@ class TestSearchApi(FixtureAPITestCase):
     def test_build_index(self, job_mock, reindex_trigger_mock):
         job_mock.fetch.side_effect = NoSuchJobError
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:build-search-index", kwargs={"pk": self.corpus.id}),
                 data={"drop": True},
@@ -445,7 +453,7 @@ class TestSearchApi(FixtureAPITestCase):
     def test_build_index_drop_false(self, job_mock, reindex_trigger_mock):
         job_mock.fetch.side_effect = NoSuchJobError
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:build-search-index", kwargs={"pk": self.corpus.id}),
                 data={"drop": False},
diff --git a/arkindex/documents/tests/test_selection_api.py b/arkindex/documents/tests/test_selection_api.py
index d45eb8e847..36970acf11 100644
--- a/arkindex/documents/tests/test_selection_api.py
+++ b/arkindex/documents/tests/test_selection_api.py
@@ -1,5 +1,5 @@
-
 import uuid
+from unittest.mock import call, patch
 
 from django.test import override_settings
 from django.urls import reverse
@@ -10,7 +10,7 @@ from arkindex.project.tests import FixtureAPITestCase
 from arkindex.users.models import Role, User
 
 
-class TestElementsAPI(FixtureAPITestCase):
+class TestSelectionAPI(FixtureAPITestCase):
 
     @classmethod
     def setUpTestData(cls):
@@ -55,7 +55,8 @@ class TestElementsAPI(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertListEqual(response.json(), ["Selection is not available on this instance."])
 
-    def test_select_element_wrong_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_select_element_wrong_corpus(self, filter_rights_mock):
         user = User.objects.create_user("nope@nope.fr")
         user.verified_email = True
         user.save()
@@ -74,6 +75,8 @@ class TestElementsAPI(FixtureAPITestCase):
         self.assertDictEqual(response.json(), {
             "ids": ["Some element IDs do not exist or you do not have permission to access them."],
         })
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(user, Corpus, Role.Guest.value))
 
     def test_select_element_wrong_id(self):
         self.client.force_login(self.user)
@@ -231,7 +234,7 @@ class TestElementsAPI(FixtureAPITestCase):
         self.user.selected_elements.add(self.page, self.vol, self.private_page)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:elements-selection"), data={"corpus": self.private_corpus.id})
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         response = self.client.get(reverse("api:elements-selection"))
@@ -246,7 +249,7 @@ class TestElementsAPI(FixtureAPITestCase):
         self.user.selected_elements.add(self.page, self.vol)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:elements-selection"), data={"corpus": bad_id})
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertEqual(response.json(), {"detail": "Not found."})
@@ -345,7 +348,7 @@ class TestElementsAPI(FixtureAPITestCase):
         self.user.selected_elements.add(self.page, self.vol, self.private_page)
         self.private_corpus.memberships.create(user=self.user, level=Role.Contributor.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:elements-selection"), data={"corpus": self.private_corpus.id})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         results = response.json()["results"]
@@ -356,7 +359,7 @@ class TestElementsAPI(FixtureAPITestCase):
         self.user.selected_elements.add(self.page, self.vol)
         bad_id = uuid.uuid4()
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:elements-selection"), data={"corpus": bad_id})
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertEqual(response.json(), {"detail": "Not found."})
diff --git a/arkindex/documents/tests/test_transcriptions.py b/arkindex/documents/tests/test_transcriptions.py
index cda2b19db5..618f9aa9ee 100644
--- a/arkindex/documents/tests/test_transcriptions.py
+++ b/arkindex/documents/tests/test_transcriptions.py
@@ -1,10 +1,12 @@
+from unittest.mock import call, patch
+
 from django.urls import reverse
 from rest_framework import status
 
 from arkindex.documents.models import Corpus, TextOrientation
 from arkindex.process.models import ProcessMode, WorkerRun, WorkerVersion
 from arkindex.project.tests import FixtureAPITestCase
-from arkindex.users.models import User
+from arkindex.users.models import Role, User
 
 
 class TestTranscriptions(FixtureAPITestCase):
@@ -28,13 +30,17 @@ class TestTranscriptions(FixtureAPITestCase):
         cls.worker_version_2 = WorkerVersion.objects.get(worker__slug="dla")
         cls.worker_run = WorkerRun.objects.filter(version=cls.worker_version_1, process__mode=ProcessMode.Workers).get()
 
-    def test_list_transcriptions_read_right(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_list_transcriptions_read_right(self, filter_rights_mock):
         # A read right on the element corpus is required to access transcriptions
         self.client.force_login(self.private_read_user)
-        url = reverse("api:element-transcriptions", kwargs={"pk": str(self.private_page.id)})
-        response = self.client.get(url)
+
+        response = self.client.get(reverse("api:element-transcriptions", kwargs={"pk": str(self.private_page.id)}))
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.private_read_user, Corpus, Role.Guest.value))
+
     def test_list_element_transcriptions(self):
         tr1 = self.page.transcriptions.get()
         tr2 = self.page.transcriptions.create(
@@ -44,7 +50,7 @@ class TestTranscriptions(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -72,7 +78,7 @@ class TestTranscriptions(FixtureAPITestCase):
     def test_list_transcriptions_recursive(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={"recursive": "true"}
@@ -94,7 +100,7 @@ class TestTranscriptions(FixtureAPITestCase):
     def test_list_transcriptions_recursive_filter_element_type(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={"recursive": "true", "element_type": "page"}
@@ -119,7 +125,7 @@ class TestTranscriptions(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={"recursive": "true", "worker_version": str(self.worker_version_2.id)}
@@ -156,7 +162,7 @@ class TestTranscriptions(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={"recursive": "true", "worker_version": False}
@@ -187,7 +193,7 @@ class TestTranscriptions(FixtureAPITestCase):
 
     def test_list_transcriptions_worker_version_validation(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={"recursive": "true", "worker_version": "oh no"}
@@ -197,7 +203,7 @@ class TestTranscriptions(FixtureAPITestCase):
 
     def test_list_transcriptions_worker_run_validation(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={"recursive": "true", "worker_run": "oh no"}
@@ -215,7 +221,7 @@ class TestTranscriptions(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={"recursive": "true", "worker_version": str(self.worker_version_2.id)}
@@ -257,7 +263,7 @@ class TestTranscriptions(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={"recursive": "true", "worker_run": str(self.worker_run.id)}
@@ -299,7 +305,7 @@ class TestTranscriptions(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:element-transcriptions", kwargs={"pk": str(self.page.id)}),
                 data={
diff --git a/arkindex/images/tests/test_image_elements.py b/arkindex/images/tests/test_image_elements.py
index 86459ccc93..2f1cb8a2a2 100644
--- a/arkindex/images/tests/test_image_elements.py
+++ b/arkindex/images/tests/test_image_elements.py
@@ -1,3 +1,5 @@
+from unittest.mock import patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -14,7 +16,7 @@ class TestImageElements(FixtureTestCase):
 
     def test_image_elements(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.get(reverse("api:image-elements", kwargs={"pk": self.img1.id}))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
@@ -28,7 +30,8 @@ class TestImageElements(FixtureTestCase):
             response = self.client.get(reverse("api:image-elements", kwargs={"pk": self.img1.id}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_image_elements_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_image_elements_acl(self, filter_rights_mock):
         """
         A user cannot list elements on corpus they have no guest access
         """
@@ -41,9 +44,12 @@ class TestImageElements(FixtureTestCase):
             image=self.img1,
             polygon=[(100, 100), (142, 142), (133, 337), (100, 100)]
         )
-        with self.assertNumQueries(10):
+        filter_rights_mock.return_value = Corpus.objects.filter(id=self.corpus.id)
+
+        with self.assertNumQueries(8):
             response = self.client.get(reverse("api:image-elements", kwargs={"pk": self.img1.id}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         results = response.json()["results"]
         self.assertEqual(len(results), 7)
         response_names = [e["name"] for e in results]
@@ -61,7 +67,7 @@ class TestImageElements(FixtureTestCase):
             polygon=[(0, 0), (42, 42), (13, 37), (0, 0)],
         )
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.get(reverse("api:image-elements", kwargs={"pk": self.img1.id}) + "?type=cake")
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
@@ -79,7 +85,7 @@ class TestImageElements(FixtureTestCase):
         vol.polygon = [(0, 0), (0, 1), (1, 1), (0, 0)]
         vol.save()
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.get(reverse("api:image-elements", kwargs={"pk": self.img1.id}) + "?folder")
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
@@ -103,7 +109,7 @@ class TestImageElements(FixtureTestCase):
                 polygon=[(i, i), (i, 200), (200, 200), (200, i), (i, i)]
             ) for i in range(40)
         ])
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.get(reverse("api:image-elements", kwargs={"pk": self.img1.id}), {"type": "duplicated"})
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
diff --git a/arkindex/ponos/admin.py b/arkindex/ponos/admin.py
index 474b809baa..d2210a4668 100644
--- a/arkindex/ponos/admin.py
+++ b/arkindex/ponos/admin.py
@@ -1,11 +1,7 @@
-from django import forms
-from django.contrib import admin, messages
-from django.core.exceptions import ValidationError
+from django.contrib import admin
 from enumfields.admin import EnumFieldListFilter
 
-from arkindex.ponos.keys import gen_nonce
-from arkindex.ponos.models import FINAL_STATES, GPU, Agent, Artifact, Farm, Secret, Task, encrypt
-from arkindex.users.admin import GroupMembershipInline, UserMembershipInline
+from arkindex.ponos.models import Artifact, Task
 
 
 class ArtifactInline(admin.TabularInline):
@@ -33,11 +29,9 @@ class TaskAdmin(admin.ModelAdmin):
         "state",
         "process_id",
         "updated",
-        "agent",
         "priority",
     )
-    list_filter = [("state", EnumFieldListFilter), "agent"]
-    list_select_related = ("agent",)
+    list_filter = [("state", EnumFieldListFilter)]
     inlines = [
         ArtifactInline,
     ]
@@ -59,12 +53,10 @@ class TaskAdmin(admin.ModelAdmin):
                     "depth",
                     "state",
                     "process",
-                    "agent",
                     "priority",
                 ),
             },
         ),
-        ("GPU", {"fields": ("requires_gpu", "gpu")}),
         ("Dates", {"fields": ("created", "updated", "expiry")}),
         (
             "Docker",
@@ -89,153 +81,4 @@ class TaskInline(admin.TabularInline):
     extra = 0
 
 
-class GPUInline(admin.TabularInline):
-    model = GPU
-    fields = ("id", "name", "index", "ram_total")
-    readonly_fields = fields
-    extra = 0
-
-
-class AgentAdmin(admin.ModelAdmin):
-    model = Agent
-    list_display = ("id", "hostname")
-    inlines = (GPUInline,)
-    readonly_fields = (
-        "id",
-        "created",
-        "updated",
-        "last_ping",
-        # Use custom admin fields to format total RAM and CPU max frequency
-        "ram_total_human",
-        "cpu_frequency_human",
-        "cpu_load",
-        "ram_load",
-        "cpu_cores",
-        "public_key",
-    )
-    fieldsets = (
-        (
-            None,
-            {"fields": ("id", "farm", "hostname")},
-        ),
-        ("Dates", {"fields": ("created", "updated", "last_ping")}),
-        ("Auth", {"fields": ("public_key",)}),
-        (
-            "Hardware",
-            {
-                "fields": (
-                    "ram_total_human",
-                    "cpu_frequency_human",
-                    "cpu_cores",
-                    "cpu_load",
-                    "ram_load",
-                )
-            },
-        ),
-    )
-
-    def has_add_permission(self, request):
-        return False
-
-    def ram_total_human(self, instance):
-        """Returns total amount of RAM expressed in GiB"""
-        return "{:.1f} GiB".format((instance.ram_total or 0) / (1024**3))
-
-    def cpu_frequency_human(self, instance):
-        """Returns CPU max frequency expressed in GHz"""
-        return "{:.1f} GHz".format((instance.cpu_frequency or 0) / 1e9)
-
-    # Overrides both admin deletion methods to show error messages when agents
-    # have tasks in non-final states. This will display a "deleted successfully"
-    # message along the errors, because the Django admin does not make it easy to
-    # remove this message, but nothing gets actually deleted.
-
-    def delete_model(self, request, agent):
-        try:
-            super().delete_model(request, agent)
-        except ValidationError as e:
-            messages.error(request, e.message)
-
-    def delete_queryset(self, request, queryset):
-        hostnames = (
-            Task.objects.filter(agent__in=queryset)
-            .exclude(state__in=FINAL_STATES)
-            .values_list("agent__hostname", flat=True)
-            .distinct()
-        )
-        if hostnames:
-            messages.error(
-                request,
-                "The following agents have tasks in non-final states and cannot be deleted: {}".format(
-                    ", ".join(list(hostnames))
-                ),
-            )
-        else:
-            super().delete_queryset(request, queryset)
-
-
-class FarmAdmin(admin.ModelAdmin):
-    model = Farm
-    list_display = ("id", "name")
-    fields = ("id", "name", "seed")
-    readonly_fields = ("id",)
-    inlines = [UserMembershipInline, GroupMembershipInline]
-
-
-class ClearTextSecretForm(forms.ModelForm):
-    """
-    Allow an administrator to edit a secret content as a cleartext value
-    A nonce is generated for newly created secrets
-    """
-
-    content = forms.CharField(widget=forms.Textarea, required=True)
-
-    def __init__(self, *args, **kwargs):
-        self.instance = kwargs.get("instance")
-        # Set initial decrypted value
-        if self.instance is not None:
-            if "initial" not in kwargs:
-                kwargs["initial"] = {}
-            kwargs["initial"]["content"] = self.instance.decrypt()
-
-        super().__init__(*args, **kwargs)
-
-    def clean_name(self):
-        # Check that name is not already used
-        secrets = Secret.objects.all()
-        if self.instance:
-            secrets = Secret.objects.exclude(pk=self.instance.pk)
-
-        if secrets.filter(name=self.cleaned_data["name"]).exists():
-            raise ValidationError("A secret with this name already exists.")
-
-        return self.cleaned_data["name"]
-
-    def clean(self):
-        if not self.cleaned_data.get("content"):
-            raise ValidationError("You must specify some content")
-
-        if self.instance:
-            nonce = self.instance.nonce
-        else:
-            nonce = gen_nonce()
-
-        # Encrypt secret content
-        encrypted_content = encrypt(nonce, self.cleaned_data["content"])
-        return {**self.cleaned_data, "nonce": nonce, "content": encrypted_content}
-
-    class Meta:
-        model = Secret
-        fields = ("id", "name", "content")
-
-
-class SecretAdmin(admin.ModelAdmin):
-    form = ClearTextSecretForm
-
-    fields = ("name", "content")
-
-
 admin.site.register(Task, TaskAdmin)
-admin.site.register(Agent, AgentAdmin)
-admin.site.register(Farm, FarmAdmin)
-admin.site.register(Secret, SecretAdmin)
diff --git a/arkindex/ponos/api.py b/arkindex/ponos/api.py
index 1d41b93ff3..5f4a225572 100644
--- a/arkindex/ponos/api.py
+++ b/arkindex/ponos/api.py
@@ -1,88 +1,20 @@
-import hashlib
-import uuid
-from collections import defaultdict
 from textwrap import dedent
 
-from django.db.models import Count, Q
 from django.shortcuts import get_object_or_404, redirect
-from django.utils import timezone
-from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema, extend_schema_view
+from drf_spectacular.utils import extend_schema, extend_schema_view
 from rest_framework.authentication import SessionAuthentication, TokenAuthentication
-from rest_framework.exceptions import ValidationError
-from rest_framework.generics import (
-    CreateAPIView,
-    ListAPIView,
-    ListCreateAPIView,
-    RetrieveAPIView,
-    RetrieveUpdateAPIView,
-    UpdateAPIView,
-)
-from rest_framework.response import Response
+from rest_framework.generics import ListCreateAPIView, RetrieveUpdateAPIView, UpdateAPIView
 from rest_framework.views import APIView
 
-from arkindex.ponos.authentication import AgentAuthentication, TaskAuthentication
-from arkindex.ponos.keys import load_private_key
-from arkindex.ponos.models import Agent, Artifact, Farm, Secret, State, Task
+from arkindex.ponos.models import Artifact, Task
 from arkindex.ponos.permissions import (
-    IsAgent,
     IsAgentOrArtifactGuest,
     IsAgentOrTaskGuest,
     IsAssignedAgentOrReadOnly,
     IsAssignedAgentOrTaskOrReadOnly,
-    IsTask,
     IsTaskAdmin,
 )
-from arkindex.ponos.renderers import PublicKeyPEMRenderer
-from arkindex.ponos.serializers import (
-    AgentActionsSerializer,
-    AgentCreateSerializer,
-    AgentDetailsSerializer,
-    AgentLightSerializer,
-    AgentStateSerializer,
-    ArtifactSerializer,
-    ClearTextSecretSerializer,
-    FarmSerializer,
-    NewTaskSerializer,
-    TaskDefinitionSerializer,
-    TaskSerializer,
-    TaskTinySerializer,
-)
-from arkindex.project.permissions import IsVerified
-from rest_framework_simplejwt.views import TokenRefreshView
-
-
-class PublicKeyEndpoint(APIView):
-    """
-    Fetch the server's public key in PEM format to perform agent registration.
-    """
-
-    authentication_classes = ()
-    renderer_classes = (PublicKeyPEMRenderer,)
-
-    @extend_schema(
-        operation_id="GetPublicKey",
-        responses={200: {"type": "string"}},
-        tags=["ponos"],
-        examples=[
-            OpenApiExample(
-                name="Public key response",
-                response_only=True,
-                media_type="application/x-pem-file",
-                status_codes=["200"],
-                value=dedent(
-                    """
-                    -----BEGIN PUBLIC KEY-----
-                    MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEmK2L6lwGzSVZwFSo0eR1z4XV6jJwjeWK
-                    YCiPKdMcQnn6u5J016k9U8xZm6XyFnmgvkhnC3wreGBTFzwLCLZCD+F3vo5x8ivz
-                    aTgNWsA3WFlqjSIEGz+PAVHSNMobBaJm
-                    -----END PUBLIC KEY-----
-                    """
-                ),
-            )
-        ],
-    )
-    def get(self, request, *args, **kwargs):
-        return Response(load_private_key().public_key())
+from arkindex.ponos.serializers import ArtifactSerializer, TaskSerializer, TaskTinySerializer
 
 
 @extend_schema(tags=["ponos"])
@@ -117,19 +49,12 @@ class TaskDetailsFromAgent(RetrieveUpdateAPIView):
     # Avoid stale read when a recently assigned agent wants to update
     # the state of one of its tasks
     queryset = Task.objects.using("default").select_related(
-        # Serialized in responses
         "agent__farm",
         "gpu",
         # Used for permission checks
         "process__corpus",
         "process__revision__repo",
     )
-    authentication_classes = (
-        TaskAuthentication,
-        AgentAuthentication,
-        TokenAuthentication,
-        SessionAuthentication,
-    )
     permission_classes = (
         # On all HTTP methods, require either any Ponos agent, an instance admin, the task itself, or guest access to the process' task
         IsAgentOrTaskGuest,
@@ -139,161 +64,6 @@ class TaskDetailsFromAgent(RetrieveUpdateAPIView):
     serializer_class = TaskSerializer
 
 
-@extend_schema_view(
-    post=extend_schema(
-        operation_id="CreateAgent",
-        tags=["ponos"],
-    ),
-)
-class AgentRegister(CreateAPIView):
-    """
-    Perform agent registration and authentication.
-    """
-    authentication_classes = ()
-    serializer_class = AgentCreateSerializer
-
-    def get_object(self):
-        if not hasattr(self.request, "data") or "public_key" not in self.request.data:
-            return
-        key_hash = uuid.UUID(
-            hashlib.md5(self.request.data["public_key"].encode("utf-8")).hexdigest()
-        )
-        return Agent.objects.filter(id=key_hash).first()
-
-    def get_serializer(self, *args, **kwargs):
-        return super().get_serializer(self.get_object(), *args, **kwargs)
-
-
-@extend_schema_view(
-    post=extend_schema(
-        operation_id="RefreshAgentToken",
-        tags=["ponos"],
-    )
-)
-class AgentTokenRefresh(TokenRefreshView):
-    """
-    Refresh a Ponos agent token when it expires
-    """
-
-
-@extend_schema(tags=["ponos"])
-class AgentDetails(RetrieveAPIView):
-    """
-    Retrieve details of an agent including its running tasks
-
-    Requires authentication with a verified e-mail address. Cannot be used with Ponos agent or task authentication.
-    """
-    authentication_classes = (TokenAuthentication, SessionAuthentication)
-    permission_classes = (IsVerified, )
-    serializer_class = AgentDetailsSerializer
-    queryset = Agent.objects.select_related("farm")
-
-
-@extend_schema(
-    description="List the state of all Ponos agents",
-    tags=["ponos"],
-)
-class AgentsState(ListAPIView):
-    """
-    List all agents on the system with their health state.
-
-    Requires authentication with a verified e-mail address. Cannot be used with Ponos agent or task authentication.
-    """
-    authentication_classes = (TokenAuthentication, SessionAuthentication)
-    permission_classes = (IsVerified, )
-    serializer_class = AgentStateSerializer
-
-    queryset = (
-        Agent.objects.all()
-        .annotate(
-            running_tasks_count=Count("tasks", filter=Q(tasks__state=State.Running))
-        )
-        .prefetch_related("farm")
-        .order_by("hostname")
-    )
-
-
-@extend_schema(
-    tags=["ponos"],
-    parameters=[
-        OpenApiParameter(
-            "cpu_load",
-            type=float,
-            description="Current CPU usage for this agent",
-            required=True
-        ),
-        OpenApiParameter(
-            "ram_load",
-            type=float,
-            description="Current RAM usage for this agent",
-            required=True
-        ),
-    ]
-)
-class AgentActions(RetrieveAPIView):
-    """
-    Fetch the next actions an agent should perform.
-
-    Requires authentication as a Ponos agent.
-    """
-
-    permission_classes = (IsAgent,)
-    authentication_classes = (AgentAuthentication,)
-    serializer_class = AgentActionsSerializer
-
-    def get_object(self):
-        return self.request.user
-
-    def retrieve(self, request, *args, **kwargs):
-        # Update agent load and last_ping timestamp
-        errors = defaultdict(list)
-        cpu_load = request.query_params.get("cpu_load")
-        ram_load = request.query_params.get("ram_load")
-        if not cpu_load:
-            errors["cpu_load"].append("This query parameter is required.")
-        if not ram_load:
-            errors["ram_load"].append("This query parameter is required.")
-        if errors:
-            raise ValidationError(errors)
-        # Handle fields validation with DRF as for a PATCH
-        agent_serializer = AgentLightSerializer(
-            self.request.user,
-            data={"cpu_load": cpu_load, "ram_load": ram_load},
-            partial=True,
-        )
-        agent_serializer.is_valid(raise_exception=True)
-        # Update agent load and last_ping attributes
-        agent_serializer.save(last_ping=timezone.now())
-        # Retrieve next tasks after the DB has been updated with the new agent load
-        response = super().retrieve(self, request, *args, **kwargs)
-        return response
-
-
-@extend_schema_view(
-    get=extend_schema(
-        operation_id="RetrieveTaskDefinition",
-        tags=["ponos"],
-    )
-)
-class TaskDefinition(RetrieveAPIView):
-    """
-    Obtain a task's definition.
-    This holds all the required data to start a task, except for the artifacts.
-
-    Requires authentication as a Ponos agent.
-    """
-
-    # We need to specify the default database to avoid stale reads
-    # when a task is updated by an agent, then the agent immediately fetches its definition
-    queryset = Task.objects.using("default").select_related("process")
-    serializer_class = TaskDefinitionSerializer
-
-    # This cannot be restricted to only the agent assigned to the task because agents need to access
-    # the parent tasks of an assigned task to download their artifacts.
-    authentication_classes = (AgentAuthentication, )
-    permission_classes = (IsAgent, )
-
-
 @extend_schema(tags=["ponos"])
 @extend_schema_view(
     get=extend_schema(
@@ -373,24 +143,6 @@ class TaskArtifactDownload(APIView):
         return redirect(artifact.s3_url)
 
 
-@extend_schema_view(
-    post=extend_schema(
-        operation_id="CreateTask",
-        tags=["ponos"],
-    )
-)
-class TaskCreate(CreateAPIView):
-    """
-    Create a task that depends on an existing task.
-
-    Requires authentication as a Ponos task. Tasks can only be created on the process of the authenticated task.
-    """
-
-    authentication_classes = (TaskAuthentication, )
-    permission_classes = (IsTask, )
-    serializer_class = NewTaskSerializer
-
-
 @extend_schema(tags=["ponos"])
 @extend_schema_view(
     put=extend_schema(
@@ -417,34 +169,3 @@ class TaskUpdate(UpdateAPIView):
     permission_classes = (IsTaskAdmin, )
     queryset = Task.objects.select_related("process__corpus", "process__revision__repo")
     serializer_class = TaskTinySerializer
-
-
-@extend_schema(tags=["ponos"])
-class SecretDetails(RetrieveAPIView):
-    """
-    Retrieve a Ponos secret's content as cleartext.
-
-    Requires authentication as a Ponos task.
-    """
-
-    authentication_classes = (TaskAuthentication, )
-    permission_classes = (IsTask, )
-    serializer_class = ClearTextSecretSerializer
-    queryset = Secret.objects.all()
-    lookup_field = "name"
-
-
-@extend_schema(tags=["ponos"])
-class FarmList(ListAPIView):
-    """
-    List all available farms.
-
-    Cannot be used with Ponos agent or task authentication.
-    """
-    serializer_class = FarmSerializer
-    queryset = Farm.objects.none()
-    authentication_classes = (TokenAuthentication, SessionAuthentication)
-    permission_classes = (IsVerified, )
-
-    def get_queryset(self):
-        return Farm.objects.available(self.request.user).order_by("name")
diff --git a/arkindex/ponos/authentication.py b/arkindex/ponos/authentication.py
index 7fc029bfe9..459b42fd92 100644
--- a/arkindex/ponos/authentication.py
+++ b/arkindex/ponos/authentication.py
@@ -1,77 +1,11 @@
 from drf_spectacular.authentication import TokenScheme
-from drf_spectacular.contrib.rest_framework_simplejwt import SimpleJWTScheme
 from rest_framework.authentication import TokenAuthentication
 from rest_framework.exceptions import AuthenticationFailed
 
-from arkindex.ponos.models import Agent, Task
-from rest_framework_simplejwt.authentication import JWTAuthentication
-from rest_framework_simplejwt.exceptions import InvalidToken
-from rest_framework_simplejwt.settings import api_settings
+from arkindex.ponos.models import Task
 from sentry_sdk import set_tag
 
 
-class AgentUser(Agent):
-    """
-    A proxy model to implement the Django User interface on a Ponos agent.
-    Allows Django REST Framework's usual permissions (like IsAuthenticated) to work.
-    """
-
-    is_staff: bool = False
-    is_superuser: bool = False
-    is_authenticated: bool = True
-    is_anonymous: bool = False
-    is_admin: bool = False
-    is_active: bool = True
-    is_agent: bool = True
-    verified_email: bool = False
-
-    @property
-    def username(self) -> str:
-        return str(self.id)
-
-    def get_username(self) -> str:
-        return self.username
-
-    class Meta:
-        proxy = True
-
-    def save(self, *args, update_fields=None, **kwargs):
-        """
-        django.contrib.auth adds a 'user_logged_in' signal and a 'update_last_login' receiver,
-        which tries to update the User.last_login field.
-        The receiver is only added when the User model has a last_login field, which depends on
-        the Django app integrating Ponos and not on Ponos itself, so we have to handle it.
-        The receiver calls .save(update_fields=['last_login']), so we remove it.
-        An empty `update_fields` will cause a save to abort silently.
-        """
-        if update_fields is not None and "last_login" in update_fields:
-            update_fields = set(update_fields) - {"last_login"}
-        return super().save(*args, update_fields=update_fields, **kwargs)
-
-
-class AgentAuthentication(JWTAuthentication):
-    """
-    Allows authenticating as a Ponos agent using a JSON Web Token.
-    """
-
-    def get_user(self, validated_token):
-        if api_settings.USER_ID_CLAIM not in validated_token:
-            raise InvalidToken("Token does not hold agent information")
-        try:
-            return AgentUser.objects.get(id=validated_token[api_settings.USER_ID_CLAIM])
-        except AgentUser.DoesNotExist:
-            raise AuthenticationFailed("Agent not found")
-
-
-class AgentAuthenticationExtension(SimpleJWTScheme):
-    """
-    drf_spectacular extension to make it recognize the AgentAuthentication.
-    """
-
-    target_class = "arkindex.ponos.authentication.AgentAuthentication"
-    name = "agentAuth"
-
-
 class TaskAuthentication(TokenAuthentication):
     keyword = "Ponos"
     model = Task
diff --git a/arkindex/ponos/keys.py b/arkindex/ponos/keys.py
deleted file mode 100644
index 137da04232..0000000000
--- a/arkindex/ponos/keys.py
+++ /dev/null
@@ -1,112 +0,0 @@
-import logging
-import os.path
-from os import urandom
-
-from cryptography.exceptions import InvalidKey
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives.asymmetric import ec
-from cryptography.hazmat.primitives.hashes import SHA256
-from cryptography.hazmat.primitives.kdf.hkdf import HKDF
-from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, load_pem_private_key
-from django.conf import settings
-
-logger = logging.getLogger(__name__)
-
-
-def gen_nonce(size=16):
-    """
-    Generates a simple nonce
-    Number size si defined in bytes (defaults to 128 bits)
-    https://cryptography.io/en/latest/glossary/#term-nonce
-    """
-    return urandom(size)
-
-
-def gen_private_key(dest_path) -> None:
-    """
-    Generates an elliptic curve private key and saves it to a local file in PEM format.
-    See https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
-
-    :param dest_path: Path to save the new key to.
-    :type dest_path: str or path-like object
-    """
-    key = ec.generate_private_key(
-        ec.SECP384R1(),
-        default_backend(),
-    )
-
-    with open(dest_path, "wb") as f:
-        f.write(
-            key.private_bytes(
-                Encoding.PEM,
-                PrivateFormat.PKCS8,
-                NoEncryption(),
-            )
-        )
-
-
-def load_private_key():
-    """
-    Load an existing private key from the path given in the ``PONOS_PRIVATE_KEY`` setting.
-
-    :returns: An elliptic curve private key instance.
-    :raises Exception: When the Django ``DEBUG`` setting is set to False
-       and the server is misconfigured or the key is not found or invalid
-    """
-
-    def _abort(message):
-        """
-        On Debug, be nice with developers, just display a warning
-        On Prod, simply crash
-        """
-        if getattr(settings, "DEBUG", False):
-            logger.warning("Please fix your security configuration: {}".format(message))
-        else:
-            raise Exception(message)
-
-    if not getattr(settings, "PONOS_PRIVATE_KEY", None):
-        return _abort("Missing setting PONOS_PRIVATE_KEY")
-
-    if not os.path.exists(settings.PONOS_PRIVATE_KEY):
-        return _abort(
-            "Invalid PONOS_PRIVATE_KEY path: {}".format(settings.PONOS_PRIVATE_KEY)
-        )
-
-    with open(settings.PONOS_PRIVATE_KEY, "rb") as f:
-        key = load_pem_private_key(
-            f.read(),
-            password=None,
-            backend=default_backend(),
-        )
-        assert isinstance(
-            key, ec.EllipticCurvePrivateKey
-        ), "Private {} key is not an ECDH key".format(settings.PONOS_PRIVATE_KEY)
-    return key
-
-
-def check_agent_key(agent_public_key, agent_derivation, seed) -> bool:
-    """
-    Authenticates a new agent using its public key and a derivation
-    of its private key with the server's public key.
-
-    :param agent_public_key: An agent's public key.
-    :type agent_public_key: cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey
-    :param agent_derivation bytes: A bytestring representing the server's public key
-       derived with the agent's private key using HKDF and holding a :class:`~`onos.models.Farm`'s seed.
-    :param seed str: The expected farm seed.
-    """
-    shared_key = load_private_key().exchange(ec.ECDH(), agent_public_key)
-
-    hkdf = HKDF(
-        algorithm=SHA256(),
-        backend=default_backend(),
-        length=32,
-        salt=None,
-        info=seed.encode("utf-8"),
-    )
-
-    try:
-        hkdf.verify(shared_key, agent_derivation)
-        return True
-    except InvalidKey:
-        return False
diff --git a/arkindex/ponos/management/__init__.py b/arkindex/ponos/management/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/arkindex/ponos/management/commands/__init__.py b/arkindex/ponos/management/commands/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/arkindex/ponos/management/commands/generate_private_key.py b/arkindex/ponos/management/commands/generate_private_key.py
deleted file mode 100644
index 9f4161b66b..0000000000
--- a/arkindex/ponos/management/commands/generate_private_key.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from django.core.management.base import BaseCommand
-
-from arkindex.ponos.keys import gen_private_key
-
-
-class Command(BaseCommand):
-    help = "Generate a Ponos server private key"
-
-    def add_arguments(self, parser):
-        parser.add_argument(
-            "path",
-            help="Destination file for the key in PEM format.",
-        )
-
-    def handle(self, *args, path=None, **kwargs):
-        gen_private_key(path)
-        self.stdout.write(
-            self.style.SUCCESS("Generated a new private key to {}".format(path))
-        )
diff --git a/arkindex/ponos/migrations/0001_initial.py b/arkindex/ponos/migrations/0001_initial.py
index e7126d647a..7a72645956 100644
--- a/arkindex/ponos/migrations/0001_initial.py
+++ b/arkindex/ponos/migrations/0001_initial.py
@@ -9,7 +9,6 @@ import enumfields.fields
 from django.contrib.postgres.operations import HStoreExtension
 from django.db import migrations, models
 
-import arkindex.ponos.keys
 import arkindex.ponos.models
 import arkindex.project.fields
 import arkindex.project.validators
@@ -77,7 +76,7 @@ class Migration(migrations.Migration):
             fields=[
                 ("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
                 ("name", models.CharField(max_length=250, unique=True)),
-                ("nonce", models.BinaryField(default=arkindex.ponos.keys.gen_nonce, max_length=16)),
+                ("nonce", models.BinaryField(default=arkindex.ponos.models.gen_nonce, max_length=16)),
                 ("content", models.BinaryField(editable=True)),
             ],
         ),
@@ -136,17 +135,6 @@ class Migration(migrations.Migration):
             name="farm",
             field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="ponos.farm"),
         ),
-        migrations.CreateModel(
-            name="AgentUser",
-            fields=[
-            ],
-            options={
-                "proxy": True,
-                "indexes": [],
-                "constraints": [],
-            },
-            bases=("ponos.agent",),
-        ),
         migrations.AddConstraint(
             model_name="workflow",
             constraint=models.CheckConstraint(check=models.Q(("finished", None), ("finished__gte", models.F("created")), _connector="OR"), name="ponos_workflow_finished_after_created"),
diff --git a/arkindex/ponos/models.py b/arkindex/ponos/models.py
index 85161882bc..ad991175c5 100644
--- a/arkindex/ponos/models.py
+++ b/arkindex/ponos/models.py
@@ -1,86 +1,37 @@
 import base64
-import logging
 import os.path
 import random
 import uuid
-from collections import namedtuple
 from datetime import timedelta
-from hashlib import sha256
+from os import urandom
 
 from botocore.exceptions import ClientError
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.postgres.fields import HStoreField
-from django.core.exceptions import ValidationError
 from django.core.validators import MinLengthValidator, MinValueValidator, RegexValidator
-from django.db import models, transaction
-from django.db.models import Count, Exists, OuterRef, Q
+from django.db import models
+from django.db.models import Q
 from django.urls import reverse
 from django.utils import timezone
 from enumfields import Enum, EnumField
 
-from arkindex.ponos.keys import gen_nonce
 from arkindex.ponos.managers import FarmManager, TaskManager
 from arkindex.project.aws import S3FileMixin
 from arkindex.project.validators import MaxValueValidator
-from arkindex.users.models import Role
-from rest_framework_simplejwt.tokens import RefreshToken
-
-# Maximum allowed time until an agent is considered inactive since last request
-AGENT_TIMEOUT = timedelta(
-    **getattr(settings, "PONOS_ACTIVE_AGENT_TIMEOUT", {"seconds": 30})
-)
-# Estimation of required resources to run a task on an agent. Defaults to 1 core and 1GB of RAM
-AGENT_SLOT = getattr(settings, "PONOS_TASK_SLOT", {"cpu": 1, "ram": 1e9})
-
-Action = namedtuple("Action", "action, task")
-Action.__doc__ = """
-Describes an instruction sent to an agent.
-Can optionally be associated with a :class:`Task`.
-"""
-
-Action.action.__doc__ = """
-Type of action to perform.
-
-:type: arkindex.ponos.models.ActionType
-"""
-
-Action.task.__doc__ = """
-Optionally associated Task, when relevant.
-
-:type: arkindex.ponos.models.Task
-"""
-
-logger = logging.getLogger(__name__)
 
 
-class ActionType(Enum):
-    """
-    Describes which action an agent should perform.
-    """
-
-    StartTask = "start_task"
-    """
-    Instruct the agent to start a new task.
-    The action must have an associated task.
-    """
+def generate_seed() -> str:
+    return "{:064x}".format(random.getrandbits(256))
 
-    StopTask = "stop_task"
-    """
-    Instruct the agent to stop a running task.
-    The action must have an associated task.
-    """
 
-    Kill = "kill"
+def gen_nonce(size=16):
     """
-    Instruct the agent to shut itself down.
-    This is the only case where the agent will have a 0 exit code.
+    Generates a simple nonce
+    Number size is defined in bytes (defaults to 128 bits)
+    https://cryptography.io/en/latest/glossary/#term-nonce
     """
-
-
-def generate_seed() -> str:
-    return "{:064x}".format(random.getrandbits(256))
+    return urandom(size)
 
 
 class Farm(models.Model):
@@ -112,19 +63,7 @@ class Farm(models.Model):
         return "Farm {}".format(self.name)
 
     def is_available(self, user) -> bool:
-        """
-        Whether this farm is visible to the user and can be used to run processes.
-        """
-        if user.is_anonymous or getattr(user, "is_agent", False):
-            return False
-
-        if user.is_admin:
-            return True
-
-        from arkindex.users.utils import get_max_level
-        level = get_max_level(user, self)
-
-        return level is not None and level >= Role.Guest.value
+        return True
 
 
 class Agent(models.Model):
@@ -149,148 +88,9 @@ class Agent(models.Model):
     ram_load = models.FloatField(null=True, blank=True)
     last_ping = models.DateTimeField(editable=False)
 
-    @property
-    def token(self) -> RefreshToken:
-        """
-        JSON Web Token for this agent.
-        """
-        return RefreshToken.for_user(self)
-
-    @property
-    def active(self) -> bool:
-        return self.last_ping >= timezone.now() - AGENT_TIMEOUT
-
     def __str__(self) -> str:
         return self.hostname
 
-    def delete(self) -> None:
-        if self.tasks.exclude(state__in=FINAL_STATES).exists():
-            raise ValidationError(
-                "This agent has one or more tasks in non-final states."
-            )
-        return super().delete()
-
-    def _estimate_new_tasks_cost(self, tasks=1):
-        """
-        Metric used to estimate the load on this agent starting new tasks.
-        Used as the cost function to minimize overall agents load while attributing tasks.
-        An agent cannot have more tasks that their CPU count, as the system cannot really process tasks faster.
-
-        :param tasks: Number of tasks to estimate the cost for.
-        :returns: A cost expressed as a percentage. If > 1, the agent would be overloaded.
-        """
-        if self.cpu_load is None or self.ram_load is None:
-            # The agent has not shared its state yet
-            return 1
-        current_tasks_count = getattr(self, "current_tasks", 0)
-        if current_tasks_count + AGENT_SLOT["cpu"] >= self.cpu_cores:
-            return 1
-
-        cpu_cost = (self.cpu_load + tasks * AGENT_SLOT["cpu"]) / self.cpu_cores
-        ram_cost = self.ram_load + tasks * AGENT_SLOT["ram"] / self.ram_total
-        return max(cpu_cost, ram_cost)
-
-    def next_tasks(self):
-        """
-        Compute the next tasks that should be run by an agent.
-        Pending tasks are attributed to agents with the lightest load first.
-
-        :returns: A list of tasks.
-        """
-        if self._estimate_new_tasks_cost() >= 1:
-            # The capacity of this agent does not allow a new task to be started
-            return []
-
-        # Filter pending task on the agent farm ordered by higher priority first and then seniority (older first)
-        pending_tasks = Task.objects.filter(
-            Q(process__farm_id=self.farm_id)
-            & Q(
-                Q(state=State.Pending, agent=None) | Q(state=State.Unscheduled, depth=0)
-            )
-        ).order_by("-priority", "updated")
-
-        if not pending_tasks.exists():
-            return []
-
-        # Retrieve active agents within the same farm, using the default database to avoid a stale-read if there are
-        # no active agents, then an agent becomes active and asks for its actions
-        active_agents = (
-            Agent.objects.using("default")
-            .filter(
-                last_ping__gte=timezone.now() - AGENT_TIMEOUT,
-                farm_id=self.farm_id,
-            )
-            .annotate(
-                current_tasks=Count("tasks", filter=Q(tasks__state=State.Running))
-            )
-        )
-
-        # List available GPUs (without any active task assigned)
-        available_gpus = list(
-            self.gpus
-            .using("default")
-            .filter(~Exists(
-                Task.objects.filter(gpu=OuterRef("pk"), state__in=ACTIVE_STATES)
-            ))
-            .only("id", "agent_id")
-        )
-
-        # Simulate the attribution of pending tasks minimizing overall load
-        attributed_tasks = {agent: [] for agent in active_agents}
-        for task in pending_tasks:
-
-            # High priority for tasks with GPU requirements
-            if task.requires_gpu:
-                if available_gpus:
-                    task.gpu = available_gpus.pop()
-                    min_cost_agent = self
-                else:
-                    # Skip tasks requiring GPU when none is available
-                    logger.info(f"No GPU available for task {task.id} - {task}")
-                    continue
-            else:
-                # Compare the cost of adding a new task on compatible agents
-                min_cost_agent = min(
-                    active_agents,
-                    key=lambda agent: agent._estimate_new_tasks_cost(tasks=len(attributed_tasks[agent]) + 1),
-                )
-
-            # Append the task to the queue of the agent with the minimal cost
-            current_tasks = attributed_tasks[min_cost_agent]
-            if min_cost_agent._estimate_new_tasks_cost(tasks=len(current_tasks) + 1) >= 1:
-                # Attributing the next task would overload the system
-                break
-            current_tasks.append(task)
-
-        # Return tasks attributed to the agent making the request
-        return attributed_tasks[self]
-
-    @transaction.atomic
-    def next_actions(self):
-        """
-        Compute the next actions to send to an agent.
-
-        This method must run in a single transaction to avoid fetching tasks that are being
-        assigned to another agent.
-
-        :returns: List of :obj:`Action`.
-        """
-        pending_tasks = self.tasks.filter(state=State.Pending)
-        next_tasks = self.next_tasks()
-        stop_tasks = self.tasks.filter(state=State.Stopping)
-
-        actions = []
-        for task in stop_tasks:
-            actions.append(Action(ActionType.StopTask, task))
-        for task in pending_tasks:
-            actions.append(Action(ActionType.StartTask, task))
-        for task in next_tasks:
-            task.agent = self
-            task.state = State.Pending
-            task.save()
-            actions.append(Action(ActionType.StartTask, task))
-        return actions
-
 
 class State(Enum):
     """
@@ -625,29 +425,6 @@ class Artifact(S3FileMixin, models.Model):
         )
 
 
-def build_aes_cipher(nonce):
-    """
-    Initialize an AES cipher using the Ponos private key
-    """
-    key_path = getattr(settings, "PONOS_PRIVATE_KEY", None)
-    assert key_path and os.path.exists(key_path), "Missing a PONOS_PRIVATE_KEY"
-    # Use the Ponos private key SHA-256 as the AES key
-    with open(settings.PONOS_PRIVATE_KEY, "rb") as f:
-        ponos_key = f.read()
-    aes_key = sha256(ponos_key).digest()
-    return Cipher(algorithms.AES(aes_key), modes.CTR(nonce))
-
-
-def encrypt(nonce, plain_text):
-    """
-    Encrypt a plain text using the AES cipher model content ciphering a text from the Ponos private key
-    """
-    cipher = build_aes_cipher(nonce)
-    encryptor = cipher.encryptor()
-    plain_text = plain_text.encode()
-    return encryptor.update(plain_text)
-
-
 class Secret(models.Model):
     """
     A secret encrypted with a derivate of the Ponos server private key (ECDH)
@@ -670,14 +447,6 @@ class Secret(models.Model):
     def __str__(self):
         return self.name
 
-    def decrypt(self):
-        """
-        Returns the plain text deciphered from the Ponos private key
-        """
-        decryptor = build_aes_cipher(self.nonce).decryptor()
-        plain_text = decryptor.update(self.content)
-        return plain_text.decode()
-
 
 class GPU(models.Model):
     """
diff --git a/arkindex/ponos/renderers.py b/arkindex/ponos/renderers.py
deleted file mode 100644
index 9fe7006191..0000000000
--- a/arkindex/ponos/renderers.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from cryptography.hazmat.primitives.asymmetric import ec
-from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
-from rest_framework.renderers import BaseRenderer
-
-
-class PublicKeyPEMRenderer(BaseRenderer):
-    """
-    A Django REST Framework renderer to serialize public keys as PEM.
-    """
-
-    media_type = "application/x-pem-file"
-    format = "pem"
-
-    def render(self, data: ec.EllipticCurvePublicKey, *args, **kwargs) -> bytes:
-        assert isinstance(data, ec.EllipticCurvePublicKey)
-        return data.public_bytes(
-            encoding=Encoding.PEM,
-            format=PublicFormat.SubjectPublicKeyInfo,
-        )
diff --git a/arkindex/ponos/serializers.py b/arkindex/ponos/serializers.py
index a040972365..0b28c78011 100644
--- a/arkindex/ponos/serializers.py
+++ b/arkindex/ponos/serializers.py
@@ -1,95 +1,24 @@
-import hashlib
 import logging
-import uuid
 from textwrap import dedent
 
 from django.conf import settings
 from django.db import transaction
 from django.db.models import Exists, OuterRef
-from django.shortcuts import reverse
 from django.utils import timezone
+from django.utils.module_loading import import_string
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError
 
-from arkindex.ponos.keys import check_agent_key
-from arkindex.ponos.models import (
-    FINAL_STATES,
-    GPU,
-    ActionType,
-    Agent,
-    Artifact,
-    Farm,
-    Secret,
-    State,
-    Task,
-    task_token_default,
-)
-from arkindex.ponos.serializer_fields import Base64Field, CurrentProcessDefault, PublicKeyField
+from arkindex.ponos.models import FINAL_STATES, Artifact, State, Task
 from arkindex.ponos.signals import task_failure
 from arkindex.project.serializer_fields import EnumField
 from arkindex.project.triggers import notify_process_completion
 
 logger = logging.getLogger(__name__)
 
-
-class FarmSerializer(serializers.ModelSerializer):
-    """
-    Serializes a :class:`~arkindex.ponos.models.Farm` instance.
-    """
-
-    class Meta:
-        model = Farm
-        fields = (
-            "id",
-            "name",
-        )
-
-
-class GPUSerializer(serializers.ModelSerializer):
-    """
-    Serializes a :class:`~arkindex.ponos.models.GPU` instance for public access.
-    """
-
-    class Meta:
-        model = GPU
-        fields = (
-            "id",
-            "index",
-            "name",
-            "ram_total",
-        )
-        read_only_fields = (
-            "id",
-            "index",
-            "name",
-            "ram_total",
-        )
-
-
-class AgentLightSerializer(serializers.ModelSerializer):
-    """
-    Serializes a :class:`~arkindex.ponos.models.Agent` instance for public access.
-    """
-
-    farm = FarmSerializer()
-    gpus = GPUSerializer(many=True, read_only=True)
-
-    class Meta:
-        model = Agent
-        fields = (
-            "id",
-            "farm",
-            "hostname",
-            "cpu_cores",
-            "cpu_frequency",
-            "ram_total",
-            "cpu_load",
-            "ram_load",
-            "last_ping",
-            "gpus",
-        )
-        read_only_fields = ("id", "last_ping")
+TaskAgentField = import_string(getattr(settings, "TASK_AGENT_FIELD", None) or "arkindex.project.serializer_fields.NullField")
+TaskGPUField = import_string(getattr(settings, "TASK_GPU_FIELD", None) or "arkindex.project.serializer_fields.NullField")
 
 
 class TaskLightSerializer(serializers.ModelSerializer):
@@ -149,8 +78,8 @@ class TaskSerializer(TaskLightSerializer):
 
     logs = serializers.SerializerMethodField()
     full_log = serializers.SerializerMethodField()
-    agent = AgentLightSerializer(read_only=True, allow_null=True)
-    gpu = GPUSerializer(read_only=True, allow_null=True)
+    agent = TaskAgentField(read_only=True, allow_null=True)
+    gpu = TaskGPUField(read_only=True, allow_null=True)
     extra_files = serializers.DictField(read_only=True)
 
     class Meta(TaskLightSerializer.Meta):
@@ -286,224 +215,6 @@ class TaskTinySerializer(TaskSerializer):
         return super().update(instance, validated_data)
 
 
-class AgentStateSerializer(AgentLightSerializer):
-    """
-    Serialize an :class:`~arkindex.ponos.models.Agent` with its state information
-    And the GPU state
-    """
-
-    running_tasks_count = serializers.IntegerField(min_value=0)
-
-    class Meta(AgentLightSerializer.Meta):
-        fields = AgentLightSerializer.Meta.fields + ("active", "running_tasks_count")
-
-
-class AgentDetailsSerializer(AgentLightSerializer):
-    """
-    Serialize an :class:`~arkindex.ponos.models.Agent` with its running tasks
-    """
-
-    running_tasks = serializers.SerializerMethodField()
-
-    class Meta(AgentLightSerializer.Meta):
-        fields = AgentLightSerializer.Meta.fields + ("active", "running_tasks")
-
-    @extend_schema_field(TaskLightSerializer(many=True))
-    def get_running_tasks(self, agent):
-        running_tasks = agent.tasks.filter(state=State.Running)
-        task_serializer = TaskLightSerializer(
-            instance=running_tasks, many=True, context=self.context
-        )
-        return task_serializer.data
-
-
-class GPUCreateSerializer(serializers.Serializer):
-    """
-    Serialize a :class:`~arkindex.ponos.models.GPU` attached to an Agent
-    """
-
-    id = serializers.UUIDField()
-    name = serializers.CharField()
-    index = serializers.IntegerField()
-    ram_total = serializers.IntegerField()
-
-
-class AgentCreateSerializer(serializers.ModelSerializer):
-    """
-    Serializer used to register a new :class:`~arkindex.ponos.models.Agent`.
-    """
-
-    public_key = PublicKeyField(write_only=True)
-    derivation = Base64Field(write_only=True)
-    access_token = serializers.CharField(read_only=True, source="token.access_token")
-    refresh_token = serializers.CharField(read_only=True, source="token")
-    gpus = GPUCreateSerializer(many=True)
-
-    class Meta:
-        model = Agent
-        fields = (
-            "id",
-            "farm",
-            "access_token",
-            "refresh_token",
-            "public_key",
-            "derivation",
-            "hostname",
-            "cpu_cores",
-            "cpu_frequency",
-            "ram_total",
-            "cpu_load",
-            "ram_load",
-            "last_ping",
-            "gpus",
-        )
-        read_only_fields = (
-            "id",
-            "access_token",
-            "refresh_token",
-            "last_ping",
-        )
-
-    def create(self, validated_data):
-        """Create the required agent and its GPUs"""
-        gpus = validated_data.pop("gpus", None)
-
-        with transaction.atomic(using="default"):
-
-            # Create Agent as usual
-            agent = super().create(validated_data)
-
-            # Create or update GPUs
-            # When an agent's private key is regenerated on the same host, an existing GPU UUID might be sent,
-            # so we need to handle existing GPUs to avoid errors.
-            for gpu_data in gpus:
-                GPU.objects.update_or_create(
-                    id=gpu_data.pop("id"), defaults=dict(agent=agent, **gpu_data)
-                )
-
-        return agent
-
-    def update(self, instance, validated_data):
-        """Create the required agent and its GPUs"""
-        gpus = validated_data.pop("gpus", None)
-
-        with transaction.atomic(using="default"):
-
-            # Update Agent as usual
-            agent = super().update(instance, validated_data)
-
-            # Delete existing GPUs
-            agent.gpus.all().delete()
-
-            # Create Gpus - the ID should not evolve as it's provided
-            # by the host
-            for gpu_data in gpus:
-                agent.gpus.create(**gpu_data)
-
-        return agent
-
-    def validate(self, data):
-        if not check_agent_key(
-            data["public_key"], data["derivation"], data["farm"].seed
-        ):
-            raise serializers.ValidationError("Key verification failed")
-        del data["derivation"]
-
-        # Turn the public key back into a string to be saved in the agent
-        # TODO: Use a custom Django model field for this?
-        data["public_key"] = PublicKeyField().to_representation(data["public_key"])
-
-        # Generate the agent ID as a MD5 hash of its public key
-        data["id"] = uuid.UUID(
-            hashlib.md5(data["public_key"].encode("utf-8")).hexdigest()
-        )
-
-        # Set last ping as agent is about to be registered
-        data["last_ping"] = timezone.now()
-
-        return data
-
-
-class ActionSerializer(serializers.Serializer):
-    """
-    Serializes an :const:`~arkindex.ponos.models.Action` instance.
-    """
-
-    action = EnumField(ActionType)
-    task_id = serializers.SerializerMethodField()
-
-    @extend_schema_field(serializers.UUIDField(allow_null=True))
-    def get_task_id(self, obj):
-        if not obj.task:
-            return
-        return str(obj.task.id)
-
-
-class AgentActionsSerializer(serializers.Serializer):
-    """
-    Serializes multiple next actions for an agent.
-    """
-
-    actions = ActionSerializer(many=True, source="next_actions")
-
-
-class TaskDefinitionSerializer(serializers.ModelSerializer):
-    """
-    Serializes a :class:`~arkindex.ponos.models.Task` instance
-    to provide startup information to assigned agents.
-    """
-
-    env = serializers.DictField(default={})
-    process_id = serializers.UUIDField()
-    agent_id = serializers.PrimaryKeyRelatedField(queryset=Agent.objects.all())
-    image_artifact_url = serializers.SerializerMethodField()
-    s3_logs_put_url = serializers.SerializerMethodField()
-    extra_files = serializers.DictField(default={})
-    state = EnumField(State)
-
-    @extend_schema_field(serializers.URLField(allow_null=True))
-    def get_image_artifact_url(self, task):
-        """Build url on the API to get a fresh download link"""
-        if not task.image_artifact:
-            return
-        return self.context["request"].build_absolute_uri(
-            reverse(
-                "api:task-artifact-download",
-                kwargs={
-                    "pk": task.image_artifact.task_id,
-                    "path": task.image_artifact.path,
-                },
-            )
-        )
-
-    @extend_schema_field(serializers.URLField(allow_null=True))
-    def get_s3_logs_put_url(self, obj):
-        if "request" not in self.context or self.context["request"].user != obj.agent:
-            return
-        return obj.logs.s3_put_url
-
-    class Meta:
-        model = Task
-        fields = (
-            "id",
-            "slug",
-            "image",
-            "command",
-            "env",
-            "shm_size",
-            "has_docker_socket",
-            "image_artifact_url",
-            "agent_id",
-            "s3_logs_put_url",
-            "parents",
-            "process_id",
-            "gpu_id",
-            "extra_files",
-            "state",
-        )
-        read_only_fields = fields
-
-
 class ArtifactSerializer(serializers.ModelSerializer):
     """
     Serializes a :class:`~arkindex.ponos.models.Artifact` instance to allow
@@ -545,94 +256,3 @@ class ArtifactSerializer(serializers.ModelSerializer):
             raise ValidationError("An artifact with this path already exists")
 
         return path
-
-
-class NewTaskSerializer(serializers.ModelSerializer):
-    """
-    Serializes a :class:`~arkindex.ponos.models.Task` instance to permit creation by a parent.
-    """
-
-    process = serializers.HiddenField(default=CurrentProcessDefault())
-    process_id = serializers.UUIDField(read_only=True)
-    command = serializers.CharField(required=False)
-    env = serializers.DictField(
-        child=serializers.CharField(), required=False, default={}
-    )
-    has_docker_socket = serializers.BooleanField(default=False)
-
-    class Meta:
-        model = Task
-        fields = (
-            "id",
-            "process",
-            "process_id",
-            "slug",
-            "parents",
-            "image",
-            "command",
-            "env",
-            "run",
-            "depth",
-            "has_docker_socket",
-        )
-        read_only_fields = (
-            "id",
-            "run",
-            "depth",
-            "process_id",
-        )
-
-    def validate(self, data):
-        parents = data["parents"]
-
-        ids = {parent.process_id for parent in parents}
-        if len(ids) != 1 or str(ids.pop()) != str(data["process"].id):
-            raise ValidationError(
-                "All parents must be in the same process as the child task"
-            )
-
-        runs = {parent.run for parent in parents}
-        if len(runs) != 1:
-            raise ValidationError(
-                "All parents must have the same run in the given process"
-            )
-        data["run"] = runs.pop()
-
-        if Task.objects.filter(
-            process=data["process"], run=data["run"], slug=data["slug"]
-        ).exists():
-            raise ValidationError(
-                f"A task with the `{data['slug']}` slug already exists in run {data['run']}."
-            )
-
-        data["depth"] = max(parent.depth for parent in parents) + 1
-
-        # Build task environment from PONOS_DEFAULT_ENV if the env is not (fully) defined in data
-        data["env"] = {**settings.PONOS_DEFAULT_ENV, **data["env"]}
-        data["env"]["ARKINDEX_PROCESS_ID"] = str(data["process"].id)
-        # Get corpus id from any of the parent tasks since it's the same for all of them
-        # TODO: Get the corpus ID from the process using a ForeignKeyField on the serializer and data["process"]
-        data["env"]["ARKINDEX_CORPUS_ID"] = parents[0].env.get("ARKINDEX_CORPUS_ID", None)
-
-        # Set the task token manually so that we can immediately copy it to the environment variables,
-        # just like what is done in Workflow.build_tasks()
-        data["token"] = task_token_default()
-        data["env"]["ARKINDEX_TASK_TOKEN"] = data["token"]
-
-        return super().validate(data)
-
-
-class ClearTextSecretSerializer(serializers.ModelSerializer):
-    """
-    Serializes a :class:`~arkindex.ponos.models.Secret` instance with its content in cleartext.
-    """
-
-    content = serializers.SerializerMethodField(read_only=True)
-
-    def get_content(self, secret) -> str:
-        return secret.decrypt()
-
-    class Meta:
-        model = Secret
-        fields = ("id", "name", "content")
-        read_only_fields = ("id", "content")
diff --git a/arkindex/ponos/tasks.py b/arkindex/ponos/tasks.py
index 0b6aca3b5a..4c53bb7be2 100644
--- a/arkindex/ponos/tasks.py
+++ b/arkindex/ponos/tasks.py
@@ -1,4 +1,8 @@
 import logging
+import tempfile
+from io import BytesIO
+from pathlib import Path
+from time import sleep
 from urllib.parse import urljoin
 
 from django.conf import settings
@@ -9,10 +13,17 @@ from django.shortcuts import reverse
 from django.template.loader import render_to_string
 from django_rq import job
 
+import docker
+from arkindex.ponos.models import State, Task
+from arkindex.ponos.utils import upload_artifact
 from arkindex.process.models import Process, WorkerActivityState
+from docker.errors import APIError, ImageNotFound
 
 logger = logging.getLogger(__name__)
 
+# Delay for polling docker task's logs in seconds
+TASK_DOCKER_POLLING = 1
+
 
 @job("default", timeout=settings.RQ_TIMEOUTS["notify_process_completion"])
 def notify_process_completion(
@@ -64,3 +75,153 @@ def notify_process_completion(
         recipient_list=[process.creator.email],
         fail_silently=False,
     )
+
+
+def upload_logs(task, text):
+    try:
+        task.logs.s3_object.upload_fileobj(
+            BytesIO(text),
+            ExtraArgs={"ContentType": "text/plain; charset=utf-8"},
+        )
+    except Exception as e:
+        logger.warning(f"Failed uploading logs for task {task}: {e}")
+
+
+def run_docker_task(client, task, temp_dir):
+    # 1. Pull the docker image
+    logger.debug(f"Pulling docker image '{task.image}'")
+    try:
+        client.images.pull(task.image)
+    except (ImageNotFound, APIError) as e:
+        # Pulling is allowed to fail when the image is already present locally (local builds)
+        if not client.images.list(task.image):
+            raise Exception(f"Image not found locally nor remotely: {e}")
+        logger.info("Remote image could not be fetched, using the local image.")
+
+    # 2. Fetch artifacts
+    logger.info("Fetching artifacts from parents")
+    for parent in task.parents.order_by("depth", "id"):
+        folder = temp_dir / str(parent.slug)
+        folder.mkdir()
+        for artifact in parent.artifacts.all():
+            path = (folder / artifact.path).resolve()
+            # Ensure path is a children of folder
+            assert str(folder.resolve()) in str(path.resolve()), "Invalid artifact path: {artifact.path}."
+            artifact.download_to(str(path))
+
+    # 3. Do run the container asynchronously
+    logger.debug("Running container")
+    kwargs = {
+        "environment": {
+            **task.env,
+            "PONOS_DATA": settings.PONOS_DATA_DIR,
+        },
+        "detach": True,
+        "network": "host",
+        "volumes": {temp_dir: {"bind": str(settings.PONOS_DATA_DIR), "mode": "rw"}},
+    }
+    artifacts_dir = temp_dir / str(task.id)
+    artifacts_dir.mkdir()
+    # The symlink will only work within docker context as bound to PONOS_DATA_DIR/<task_uuid>/
+    (temp_dir / "current").symlink_to(Path(settings.PONOS_DATA_DIR) / str(task.id))
+
+    if task.requires_gpu:
+        # Assign all GPUs to that container
+        # https://github.com/docker/docker-py/issues/2395#issuecomment-907243275
+        kwargs["device_requests"] = [
+            docker.types.DeviceRequest(count=-1, capabilities=[["gpu"]])
+        ]
+        logger.info("Starting container with GPU support")
+    if task.command is not None:
+        kwargs["command"] = task.command
+    container = client.containers.run(task.image, **kwargs)
+    while container.status == "created":
+        container.reload()
+    task.state = State.Running
+    task.save()
+
+    # 4. Read logs
+    logger.debug("Reading logs from the docker container")
+    previous_logs = b""
+    while container.status == "running":
+        logs = container.logs()
+        if logs != previous_logs:
+            upload_logs(task, logs)
+            previous_logs = logs
+        # Handle a task that is being stopped during execution
+        task.refresh_from_db()
+        if task.state == State.Stopping:
+            container.stop()
+            task.state = State.Stopped
+            task.save()
+            break
+        sleep(TASK_DOCKER_POLLING)
+        container.reload()
+    # Upload logs one last time so we do not miss any data
+    upload_logs(task, container.logs())
+
+    # 5. Retrieve the state of the container
+    container.reload()
+    exit_code = container.attrs["State"]["ExitCode"]
+    if exit_code != 0:
+        logger.info("Task failed")
+        task.state = State.Failed
+        task.save()
+        return
+
+    task.state = State.Completed
+    task.save()
+
+    # 6. Upload artifacts
+    logger.info(f"Uploading artifacts for task {task}")
+
+    for path in Path(artifacts_dir).glob("**/*"):
+        if path.is_dir():
+            continue
+        try:
+            upload_artifact(task, path, artifacts_dir)
+        except Exception as e:
+            logger.warning(
+                f"Failed uploading artifact {path} for task {task}: {e}"
+            )
+
+
+@job("tasks", timeout=settings.RQ_TIMEOUTS["task"])
+def run_task_rq(task: Task):
+    """Run a single task in RQ"""
+    # Update task and parents from the DB
+    task.refresh_from_db()
+    parents = list(task.parents.order_by("depth", "id"))
+
+    client = docker.from_env()
+
+    if not task.image:
+        raise ValueError(f"Task {task} has no docker image.")
+
+    if task.state != State.Pending:
+        raise ValueError(f"Task {task} must be in pending state to run in RQ.")
+
+    # Automatically update children in case an error occurred
+    if (parent_state := next(
+        (parent.state for parent in parents if parent.state in (State.Stopped, State.Error, State.Failed)),
+        None
+    )) is not None:
+        task.state = parent_state
+        task.save()
+        return
+
+    with tempfile.TemporaryDirectory(suffix=f"_{task.id}") as temp_dir:
+        try:
+            run_docker_task(client, task, Path(temp_dir))
+        except Exception as e:
+            logger.error(f"An unexpected error occurred, updating state to Error: {e}")
+            task.state = State.Error
+            task.save()
+            # Add unexpected error details to task logs
+            text = BytesIO()
+            if task.logs.exists():
+                task.logs.s3_object.download_fileobj(text)
+            text = text.getvalue()
+            text += f"\nPonos exception: {e}".encode()
+            upload_logs(task, text)
+            raise e
diff --git a/arkindex/ponos/tests/test_admin.py b/arkindex/ponos/tests/test_admin.py
deleted file mode 100644
index 06d68609a7..0000000000
--- a/arkindex/ponos/tests/test_admin.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from pathlib import Path
-
-from django.test import TestCase, override_settings
-
-from arkindex.ponos.admin import ClearTextSecretForm, SecretAdmin
-from arkindex.ponos.models import Secret, encrypt
-
-PONOS_PRIVATE_KEY = Path(__file__).absolute().parent / "fixtures" / "ponos.key"
-
-
-class TestAdmin(TestCase):
-
-    @override_settings(PONOS_PRIVATE_KEY=PONOS_PRIVATE_KEY)
-    def test_admin_read_secret(self):
-        """
-        Admin form display the decrypted content of the stored secrets
-        """
-        nonce = b"1337" * 4
-        encrypted = encrypt(nonce, "Shhhh")
-        secret = Secret.objects.create(
-            name="important_secret",
-            nonce=b"1337" * 4,
-            content=encrypted,
-        )
-        self.assertEqual(secret.content, b"\xa3\xda\x9b\x91#")
-        form = ClearTextSecretForm(instance=secret)
-        self.assertEqual(form.initial.get("content"), "Shhhh")
-
-    @override_settings(PONOS_PRIVATE_KEY=PONOS_PRIVATE_KEY)
-    def test_admin_updates_secret(self):
-        nonce = b"1337" * 4
-        encrypted = encrypt(nonce, "Shhhh")
-        secret = Secret.objects.create(
-            name="important_secret",
-            nonce=b"1337" * 4,
-            content=encrypted,
-        )
-
-        secret_admin = SecretAdmin(model=Secret, admin_site=None)
-        form = ClearTextSecretForm(
-            data={"id": secret.id, "name": secret.name, "content": "Ah"},
-            instance=secret,
-        )
-        self.assertEqual(secret.content, b"\xa3\xda\x9b\x91#")
-        secret_admin.save_form(request=None, form=form, change=True)
-        self.assertEqual(secret.content, b"\xb1\xda")
-        self.assertEqual(b"\xb1\xda", encrypt(secret.nonce, "Ah"))
diff --git a/arkindex/ponos/tests/test_api.py b/arkindex/ponos/tests/test_api.py
index 7456f22a26..713836fb26 100644
--- a/arkindex/ponos/tests/test_api.py
+++ b/arkindex/ponos/tests/test_api.py
@@ -1,42 +1,17 @@
-import base64
-import hashlib
-import random
-import uuid
 from io import BytesIO
-from pathlib import Path
-from textwrap import dedent
-from unittest.mock import PropertyMock, call, patch, seal
+from unittest import expectedFailure
+from unittest.mock import call, patch, seal
 
 from botocore.exceptions import ClientError
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives.asymmetric import ec
-from cryptography.hazmat.primitives.hashes import SHA256
-from cryptography.hazmat.primitives.kdf.hkdf import HKDF
-from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, load_pem_private_key
-from django.conf import settings
-from django.core import mail
 from django.test import override_settings
 from django.urls import reverse
-from django.utils import timezone
 from rest_framework import status
 
 from arkindex.documents.models import Corpus
-from arkindex.ponos.api import timezone as api_tz
-from arkindex.ponos.authentication import AgentUser
-from arkindex.ponos.models import ACTIVE_STATES, FINAL_STATES, GPU, Agent, Farm, Secret, State, Task, encrypt
-from arkindex.ponos.tasks import notify_process_completion
-from arkindex.process.models import Process, ProcessMode, Revision, WorkerActivityState, WorkerVersion
+from arkindex.ponos.models import FINAL_STATES, State
+from arkindex.process.models import Process, ProcessMode, Revision, WorkerVersion
 from arkindex.project.tests import FixtureAPITestCase
-from arkindex.project.tools import build_public_key
 from arkindex.users.models import Right, Role, User
-from rest_framework_simplejwt.tokens import AccessToken, RefreshToken
-
-PONOS_PRIVATE_KEY = Path(__file__).absolute().parent / "fixtures" / "ponos.key"
-
-
-# Helper to format a datetime as output by DRF
-def str_date(d):
-    return d.isoformat().replace("+00:00", "Z")
 
 
 @override_settings(PONOS_LOG_TAIL=42)
@@ -47,21 +22,6 @@ class TestAPI(FixtureAPITestCase):
         super().setUpTestData()
         cls.page_1 = cls.corpus.elements.get(name="Volume 1, page 1r")
         cls.page_2 = cls.corpus.elements.get(name="Volume 1, page 2r")
-        cls.default_farm = Farm.objects.create(name="Default farm")
-        cls.wheat_farm = Farm.objects.get(name="Wheat farm")
-        pubkey = build_public_key()
-        cls.agent = AgentUser.objects.create(
-            id=uuid.UUID(hashlib.md5(pubkey.encode("utf-8")).hexdigest()),
-            farm=cls.wheat_farm,
-            hostname="ghostname",
-            cpu_cores=2,
-            cpu_frequency=1e9,
-            public_key=pubkey,
-            ram_total=2e9,
-            last_ping=timezone.now(),
-            cpu_load=.1,
-            ram_load=.1e9,
-        )
         cls.rev = Revision.objects.first()
         cls.process = Process.objects.get(mode=ProcessMode.Workers)
         cls.process.run()
@@ -75,30 +35,17 @@ class TestAPI(FixtureAPITestCase):
             mode=ProcessMode.Workers,
             creator=new_user,
             corpus=new_corpus,
-            farm=cls.default_farm,
         )
         cls.process2.run()
         cls.task4 = cls.process2.tasks.first()
 
         cls.dla = WorkerVersion.objects.get(worker__slug="dla")
         cls.recognizer = WorkerVersion.objects.get(worker__slug="reco")
-        cls.gpu1 = cls.agent.gpus.create(
-            id="108c6524-c63a-4811-bbed-9723d32a0688",
-            name="GPU1",
-            index=0,
-            ram_total=2 * 1024 * 1024 * 1024,
-        )
-        cls.gpu2 = cls.agent.gpus.create(
-            id="f30d1407-92bb-484b-84b0-0b8bae41ca91",
-            name="GPU2",
-            index=1,
-            ram_total=8 * 1024 * 1024 * 1024,
-        )
 
     def test_task_details_requires_login(self):
         with self.assertNumQueries(0):
             resp = self.client.get(reverse("api:task-details", args=[self.task1.id]))
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
     @patch("arkindex.project.aws.s3")
     def test_task_details_own_task(self, s3_mock):
@@ -164,6 +111,7 @@ class TestAPI(FixtureAPITestCase):
             resp = self.client.get(reverse("api:task-details", args=[self.task1.id]))
             self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_task_details_requires_process_guest(self):
         self.process.creator = self.superuser
         self.process.save()
@@ -195,7 +143,7 @@ class TestAPI(FixtureAPITestCase):
             with self.subTest(role=role):
                 self.corpus.memberships.filter(user=self.user).update(level=role.value)
 
-                with self.assertNumQueries(6):
+                with self.assertNumQueries(4):
                     resp = self.client.get(reverse("api:task-details", args=[self.task1.id]))
                     self.assertEqual(resp.status_code, status.HTTP_200_OK)
 
@@ -249,7 +197,7 @@ class TestAPI(FixtureAPITestCase):
                 membership.level = role.value
                 membership.save()
 
-                with self.assertNumQueries(7):
+                with self.assertNumQueries(4):
                     resp = self.client.get(reverse("api:task-details", args=[self.task1.id]))
                     self.assertEqual(resp.status_code, status.HTTP_200_OK)
 
@@ -321,2476 +269,387 @@ class TestAPI(FixtureAPITestCase):
             call("get_object", Params={"Bucket": "ponos", "Key": "somelog"}),
         )
 
-    def test_task_definition(self):
-        with self.assertNumQueries(3):
-            response = self.client.get(
-                reverse("api:task-definition", args=[self.task1.id]),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.assertDictEqual(response.json(), {
-            "id": str(self.task1.id),
-            "slug": "initialisation",
-            "parents": [],
-            "image": settings.ARKINDEX_TASKS_IMAGE,
-            "command": f"python -m arkindex_tasks.init_elements {self.process.id} --chunks-number 1",
-            "shm_size": None,
-            "env": {
-                "ARKINDEX_API_CSRF_COOKIE": "arkindex.csrf",
-                "ARKINDEX_API_TOKEN": "deadbeefTestToken",
-                "ARKINDEX_API_URL": "http://localhost:8000/api/v1/",
-                "ARKINDEX_CORPUS_ID": str(self.corpus.id),
-                "ARKINDEX_PROCESS_ID": str(self.process.id),
-                "ARKINDEX_TASK_TOKEN": self.task1.token,
-            },
-            "has_docker_socket": False,
-            "image_artifact_url": None,
-            "agent_id": None,
-            "gpu_id": None,
-            "process_id": str(self.process.id),
-            "extra_files": {},
-            "s3_logs_put_url": None,
-            "state": State.Unscheduled.value,
-        })
-
-    def test_task_definition_requires_login(self):
+    def test_update_task_requires_login(self):
         with self.assertNumQueries(0):
-            response = self.client.get(reverse("api:task-definition", args=[self.task1.id]))
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
 
-    def test_task_definition_requires_agent(self):
+    def test_update_task_requires_verified(self):
+        self.user.verified_email = False
+        self.user.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(0):
-            response = self.client.get(reverse("api:task-definition", args=[self.task1.id]))
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
 
-    def test_task_definition_task_forbidden(self):
+        with self.assertNumQueries(2):
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+    def test_update_task_forbids_task(self):
         with self.assertNumQueries(0):
-            response = self.client.get(
-                reverse("api:task-definition", args=[self.task1.id]),
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
                 HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
             )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
 
-    def test_task_definition_image_artifact(self):
-        self.task1.image_artifact = self.task1.artifacts.create(
-            path="path/to/image.tar",
-            size=1234567,
-            content_type="application/x-tar",
-        )
-        self.task1.save()
+    @expectedFailure
+    def test_update_task_requires_process_admin_corpus(self):
+        self.process.creator = self.superuser
+        self.process.save()
+        self.corpus.public = False
+        self.corpus.save()
+        self.client.force_login(self.user)
 
-        with self.assertNumQueries(4):
-            response = self.client.get(
-                reverse("api:task-definition", args=[self.task1.id]),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
+        for role in [None, Role.Guest, Role.Contributor]:
+            with self.subTest(role=role):
+                self.corpus.memberships.filter(user=self.user).delete()
+                if role:
+                    self.corpus.memberships.create(user=self.user, level=role.value)
 
-        self.assertDictEqual(response.json(), {
-            "id": str(self.task1.id),
-            "slug": "initialisation",
-            "parents": [],
-            "image": settings.ARKINDEX_TASKS_IMAGE,
-            "command": f"python -m arkindex_tasks.init_elements {self.process.id} --chunks-number 1",
-            "shm_size": None,
-            "env": {
-                "ARKINDEX_API_CSRF_COOKIE": "arkindex.csrf",
-                "ARKINDEX_API_TOKEN": "deadbeefTestToken",
-                "ARKINDEX_API_URL": "http://localhost:8000/api/v1/",
-                "ARKINDEX_CORPUS_ID": str(self.corpus.id),
-                "ARKINDEX_PROCESS_ID": str(self.process.id),
-                "ARKINDEX_TASK_TOKEN": self.task1.token,
-            },
-            "has_docker_socket": False,
-            "image_artifact_url": f"http://testserver/api/v1/task/{str(self.task1.id)}/artifact/path/to/image.tar",
-            "agent_id": None,
-            "gpu_id": None,
-            "process_id": str(self.process.id),
-            "extra_files": {},
-            "s3_logs_put_url": None,
-            "state": State.Unscheduled.value,
-        })
+                with self.assertNumQueries(5):
+                    resp = self.client.put(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
+
+    @expectedFailure
+    def test_update_task_requires_process_admin_repo(self):
+        self.process.mode = ProcessMode.Repository
+        self.process.corpus = None
+        self.process.revision = self.rev
+        self.process.creator = self.superuser
+        self.process.save()
+        self.client.force_login(self.user)
+
+        for role in [None, Role.Guest, Role.Contributor]:
+            with self.subTest(role=role):
+                self.rev.repo.memberships.filter(user=self.user).delete()
+                if role:
+                    self.rev.repo.memberships.create(user=self.user, level=role.value)
+
+                with self.assertNumQueries(5):
+                    resp = self.client.put(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_task_definition_shm_size(self):
-        self.task1.shm_size = "128g"
+    def test_update_running_task_state_stopping(self):
+        self.task1.state = State.Running
         self.task1.save()
+        self.client.force_login(self.superuser)
 
-        with self.assertNumQueries(3):
-            response = self.client.get(
-                reverse("api:task-definition", args=[self.task1.id]),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+        with self.assertNumQueries(4):
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
             )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(resp.status_code, status.HTTP_200_OK)
 
-        self.assertDictEqual(response.json(), {
+        self.assertDictEqual(resp.json(), {
             "id": str(self.task1.id),
-            "slug": "initialisation",
-            "parents": [],
-            "image": settings.ARKINDEX_TASKS_IMAGE,
-            "command": f"python -m arkindex_tasks.init_elements {self.process.id} --chunks-number 1",
-            "shm_size": "128g",
-            "env": {
-                "ARKINDEX_API_CSRF_COOKIE": "arkindex.csrf",
-                "ARKINDEX_API_TOKEN": "deadbeefTestToken",
-                "ARKINDEX_API_URL": "http://localhost:8000/api/v1/",
-                "ARKINDEX_CORPUS_ID": str(self.corpus.id),
-                "ARKINDEX_PROCESS_ID": str(self.process.id),
-                "ARKINDEX_TASK_TOKEN": self.task1.token,
-            },
-            "has_docker_socket": False,
-            "image_artifact_url": None,
-            "agent_id": None,
-            "gpu_id": None,
-            "process_id": str(self.process.id),
-            "extra_files": {},
-            "s3_logs_put_url": None,
-            "state": State.Unscheduled.value,
+            "state": State.Stopping.value,
         })
+        self.task1.refresh_from_db()
+        self.assertEqual(self.task1.state, State.Stopping)
 
-    @patch("arkindex.project.aws.s3")
-    @patch("arkindex.ponos.serializers.timezone")
-    def test_task_final_updates_process_finished(
-        self, timezone_mock, s3_mock
-    ):
-        """
-        Updating a task to a final state should update the process's completion date
-        if it causes the whole process to be finished.
-        """
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"Failed successfully")
-        }
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-
-        expected_datetime = timezone.datetime(3000, 1, 1, 12).astimezone()
-        timezone_mock.now.return_value = expected_datetime
-
-        self.task1.agent = self.agent
-        self.task1.state = State.Completed
-        self.task1.save()
-        self.task2.agent = self.agent
-        self.task2.state = State.Completed
-        self.task2.save()
+    def test_update_non_running_task_state_stopping(self):
+        states = list(State)
+        states.remove(State.Running)
+        self.client.force_login(self.superuser)
 
-        for state in FINAL_STATES:
+        for state in states:
             with self.subTest(state=state):
-                if state == State.Stopped:
-                    self.task3.state = State.Stopping
-                else:
-                    self.task3.state = State.Running
-                self.task3.agent = self.agent
-                self.task3.save()
-
-                self.process.finished = None
-                # No email is sent with mode Repository
-                self.process.mode = ProcessMode.Repository
-                self.process.corpus = None
-                self.process.save()
-
-                with self.assertNumQueries(11):
-                    resp = self.client.patch(
-                        reverse("api:task-details", args=[self.task3.id]),
-                        data={"state": state.value},
-                        HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+                self.task1.state = state
+                self.task1.save()
+
+                with self.assertNumQueries(3):
+                    resp = self.client.put(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
                     )
-                    self.assertEqual(resp.status_code, status.HTTP_200_OK)
+                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+                self.assertDictEqual(
+                    resp.json(),
+                    {"state": [f"Transition from state {state} to state Stopping is forbidden."]},
+                )
 
-                self.task3.refresh_from_db()
-                self.assertEqual(self.task3.state, state)
+                self.task1.refresh_from_db()
+                self.assertEqual(self.task1.state, state)
 
-                self.process.refresh_from_db()
-                self.assertEqual(self.process.finished, expected_datetime)
+    def test_update_final_task_state_pending(self):
+        self.task1.state = State.Completed
+        self.task1.save()
+        self.client.force_login(self.superuser)
 
-                # No email is sent for processes of type Repository
-                self.assertEqual(len(mail.outbox), 0)
+        with self.assertNumQueries(5):
+            resp = self.client.put(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Pending.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_200_OK)
 
-    @patch("arkindex.project.aws.s3")
-    @patch("arkindex.ponos.serializers.timezone")
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    def test_process_failed_finished(self, async_notify_mock, timezone_mock, s3_mock):
-        """
-        When a task fails, its children stay unscheduled, and nothing will ever be running
-        in the process anymore. It should be marked as finished.
-        """
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"Failed successfully")
-        }
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-        seal(s3_mock)
+        self.task1.refresh_from_db()
+        self.assertEqual(self.task1.state, State.Pending)
+        self.assertIsNone(self.task1.agent)
+        self.assertIsNone(self.task1.gpu)
 
-        expected_datetime = timezone.datetime(3000, 1, 1, 12).astimezone()
-        timezone_mock.now.return_value = expected_datetime
-        seal(timezone_mock)
+    def test_update_non_final_task_state_pending(self):
+        states = set(State) - set(FINAL_STATES)
+        self.client.force_login(self.superuser)
 
-        self.task1.state = State.Completed
-        self.task3.state = State.Unscheduled
-        self.task1.save()
-        self.task3.save()
-
-        for state in set(FINAL_STATES) - {State.Completed}:
-            self.task2.agent = self.agent
-            if state == State.Stopped:
-                self.task2.state = State.Stopping
-            else:
-                self.task2.state = State.Running
-            self.task2.save()
+        for state in states:
             with self.subTest(state=state):
-                self.process.finished = None
-                self.process.save()
+                self.task1.state = state
+                self.task1.save()
 
-                with self.assertNumQueries(11):
-                    resp = self.client.patch(
-                        reverse("api:task-details", args=[self.task2.id]),
-                        data={"state": state.value},
-                        HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+                with self.assertNumQueries(3):
+                    resp = self.client.put(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Pending.value},
                     )
-                    self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-                self.task2.refresh_from_db()
-                self.assertEqual(self.task2.state, state)
-
-                self.process.refresh_from_db()
-                self.assertEqual(self.process.finished, expected_datetime)
+                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
 
-    @patch("arkindex.project.aws.s3")
-    @patch("arkindex.ponos.serializers.timezone")
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    @override_settings(PUBLIC_HOSTNAME="http://ark.teklia/")
-    def test_process_finished_email_failure(
-        self, async_notify, timezone_mock, s3_mock
-    ):
-        async_notify.side_effect = notify_process_completion
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"Failed successfully")
-        }
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-        seal(s3_mock)
-        expected_datetime = timezone.datetime(3000, 1, 1, 12).astimezone()
-        timezone_mock.now.return_value = expected_datetime
-        seal(timezone_mock)
-
-        self.process.elements.set((self.page_1, self.page_2))
-        self.process.activities.create(
-            element=self.page_1,
-            worker_version=self.dla,
-            state=WorkerActivityState.Error.value,
-        )
-        self.process.activities.create(
-            element=self.page_1,
-            worker_version=self.recognizer,
-            state=WorkerActivityState.Error.value,
-        )
+                self.assertDictEqual(
+                    resp.json(),
+                    {"state": [f"Transition from state {state} to state Pending is forbidden."]}
+                )
+                self.task1.refresh_from_db()
+                self.assertEqual(self.task1.state, state)
 
-        self.process.finished = None
-        self.process.name = None
-        self.process.save()
-        self.process.tasks.update(agent=self.agent, state=State.Completed)
-        self.process.tasks.filter(id=self.task3.id).update(state=State.Running)
-        with self.assertNumQueries(13):
+    def test_partial_update_task_from_agent_requires_login(self):
+        with self.assertNumQueries(0):
             resp = self.client.patch(
-                reverse("api:task-details", args=[self.task3.id]),
-                data={"state": State.Failed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+                reverse("api:task-details", args=[self.task1.id]),
+                data={"state": State.Completed.value},
             )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.assertEqual(len(mail.outbox), 1)
-        message = mail.outbox[0]
-        self.assertEqual(message.subject, f"Your process {self.process.id} finished with failures")
-        self.assertListEqual(message.to, [self.process.creator.email])
-        self.assertEqual(message.body, dedent(f"""
-        Hello Test user,
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
-        A process you created just finished.
-        ID: {self.process.id}
-        Overall state: Failed
+    def test_partial_update_task_from_agent_forbids_users(self):
+        self.client.force_login(self.user)
+        with self.assertNumQueries(2):
+            resp = self.client.patch(
+                reverse("api:task-details", args=[self.task1.id]),
+                data={"state": State.Completed.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
-        Tasks summary:
-        * {self.task1.slug}: Completed
-        * {self.task2.slug}: Completed
-        * {self.task3.slug}: Failed
+    def test_partial_update_task_from_agent_forbids_task(self):
+        with self.assertNumQueries(1):
+            resp = self.client.patch(
+                reverse("api:task-details", args=[self.task1.id]),
+                data={"state": State.Completed.value},
+                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
+            )
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
+    def test_partial_update_task_requires_login(self):
+        with self.assertNumQueries(0):
+            resp = self.client.patch(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
 
-        Your process has 2 workers with erroneous elements:
-        - Document layout analyser worker with 100% failures (1/1)
+    def test_partial_update_task_requires_verified(self):
+        self.user.verified_email = False
+        self.user.save()
+        self.client.force_login(self.user)
 
-        - Recognizer worker with 100% failures (1/1)
+        with self.assertNumQueries(2):
+            resp = self.client.patch(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+            )
+            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
+    def test_partial_update_task_forbids_task(self):
+        with self.assertNumQueries(0):
+            resp = self.client.patch(
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
+                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
+            )
+            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
 
-        You can see all the details and logs here: http://ark.teklia/process/{self.process.id}/0
+    @expectedFailure
+    def test_partial_update_task_requires_process_admin_corpus(self):
+        self.process.creator = self.superuser
+        self.process.save()
+        self.corpus.public = False
+        self.corpus.save()
+        self.client.force_login(self.user)
 
-        --
-        Arkindex
+        for role in [None, Role.Guest, Role.Contributor]:
+            with self.subTest(role=role):
+                self.corpus.memberships.filter(user=self.user).delete()
+                if role:
+                    self.corpus.memberships.create(user=self.user, level=role.value)
 
-        """))
+                with self.assertNumQueries(5):
+                    resp = self.client.patch(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
-    @patch("arkindex.project.aws.s3")
-    @patch("arkindex.ponos.serializers.timezone")
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    @override_settings(PUBLIC_HOSTNAME="http://ark.teklia/")
-    def test_process_finished_email_stopped(
-        self, async_notify, timezone_mock, s3_mock
-    ):
-        async_notify.side_effect = notify_process_completion
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"Failed successfully")
-        }
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-        seal(s3_mock)
-        expected_datetime = timezone.datetime(3000, 1, 1, 12).astimezone()
-        timezone_mock.now.return_value = expected_datetime
-        seal(timezone_mock)
-
-        # Add some worker activities
-        self.process.elements.set((self.page_1, self.page_2))
-        self.process.activities.create(
-            element=self.page_1,
-            worker_version=self.dla,
-            state=WorkerActivityState.Error.value,
-        )
-        self.process.activities.create(
-            element=self.page_2,
-            worker_version=self.dla,
-            state=WorkerActivityState.Processed.value,
-        )
-        self.process.activities.create(
-            element=self.page_1,
-            worker_version=self.recognizer,
-            state=WorkerActivityState.Queued.value,
-        )
-
-        self.process.tasks.update(agent=self.agent, state=State.Completed)
-        self.process.tasks.filter(id=self.task3.id).update(state=State.Stopping)
-        with self.assertNumQueries(13):
-            resp = self.client.patch(
-                reverse("api:task-details", args=[self.task3.id]),
-                data={"state": State.Stopped.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.assertEqual(len(mail.outbox), 1)
-        message = mail.outbox[0]
-        self.assertEqual(message.subject, f"Your process {self.process.name} finished because it was stopped")
-        self.assertListEqual(message.to, [self.process.creator.email])
-        self.assertEqual(message.body, dedent(f"""
-        Hello Test user,
-
-        A process you created just finished.
-        ID: {self.process.id}
-        Overall state: Stopped
-
-        Tasks summary:
-        * {self.task1.slug}: Completed
-        * {self.task2.slug}: Completed
-        * {self.task3.slug}: Stopped
-
-
-        Your process has 1 worker with erroneous elements:
-        - Document layout analyser worker with 50% failures (1/2)
-
-
-        You can see all the details and logs here: http://ark.teklia/process/{self.process.id}/0
+    @expectedFailure
+    def test_partial_update_task_requires_process_admin_repo(self):
+        self.process.mode = ProcessMode.Repository
+        self.process.corpus = None
+        self.process.revision = self.rev
+        self.process.creator = self.superuser
+        self.process.save()
+        self.client.force_login(self.user)
 
-        --
-        Arkindex
+        for role in [None, Role.Guest, Role.Contributor]:
+            with self.subTest(role=role):
+                self.rev.repo.memberships.filter(user=self.user).delete()
+                if role:
+                    self.rev.repo.memberships.create(user=self.user, level=role.value)
 
-        """))
+                with self.assertNumQueries(5):
+                    resp = self.client.patch(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
-    @patch("arkindex.project.aws.s3")
-    @patch("arkindex.ponos.serializers.timezone")
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    @override_settings(PUBLIC_HOSTNAME="http://ark.teklia/")
-    def test_process_finished_email_error(
-        self, async_notify, timezone_mock, s3_mock
-    ):
-        async_notify.side_effect = notify_process_completion
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"Failed successfully")
-        }
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-        seal(s3_mock)
-        expected_datetime = timezone.datetime(3000, 1, 1, 12).astimezone()
-        timezone_mock.now.return_value = expected_datetime
-        seal(timezone_mock)
+    def test_partial_update_running_task_state_stopping(self):
+        self.task1.state = State.Running
+        self.task1.save()
+        self.client.force_login(self.superuser)
 
-        self.process.finished = None
-        self.process.name = "wonderful process"
-        self.process.save()
-        self.process.tasks.update(agent=self.agent, state=State.Error)
-        self.process.tasks.filter(id=self.task3.id).update(state=State.Running)
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(4):
             resp = self.client.patch(
-                reverse("api:task-details", args=[self.task3.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Stopping.value},
             )
             self.assertEqual(resp.status_code, status.HTTP_200_OK)
 
-        self.assertEqual(len(mail.outbox), 1)
-        message = mail.outbox[0]
-        self.assertEqual(message.subject, "Your process wonderful process finished with errors")
-        self.assertListEqual(message.to, [self.process.creator.email])
-        self.assertEqual(message.body, dedent(f"""
-        Hello Test user,
-
-        A process you created just finished.
-        ID: {self.process.id}
-        Overall state: Error
+        self.assertDictEqual(resp.json(), {
+            "id": str(self.task1.id),
+            "state": State.Stopping.value,
+        })
+        self.task1.refresh_from_db()
+        self.assertEqual(self.task1.state, State.Stopping)
 
-        Tasks summary:
-        * {self.task1.slug}: Error
-        * {self.task2.slug}: Error
-        * {self.task3.slug}: Completed
+    def test_partial_update_non_running_task_state_stopping(self):
+        states = list(State)
+        states.remove(State.Running)
+        self.task1.save()
+        self.client.force_login(self.superuser)
 
-        You can see all the details and logs here: http://ark.teklia/process/{self.process.id}/0
+        for state in states:
+            with self.subTest(state=state):
+                self.task1.state = state
+                self.task1.save()
 
-        --
-        Arkindex
+                with self.assertNumQueries(3):
+                    resp = self.client.patch(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Stopping.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+                self.assertDictEqual(
+                    resp.json(),
+                    {"state": [f"Transition from state {state} to state Stopping is forbidden."]}
+                )
 
-        """))
+                self.task1.refresh_from_db()
+                self.assertEqual(self.task1.state, state)
 
-    @patch("arkindex.project.aws.s3")
-    @patch("arkindex.ponos.serializers.timezone")
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    @override_settings(PUBLIC_HOSTNAME="http://ark.teklia/")
-    def test_process_finished_email_completed_third_run(
-        self, async_notify, timezone_mock, s3_mock
-    ):
-        async_notify.side_effect = notify_process_completion
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"Failed successfully")
-        }
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-        seal(s3_mock)
-        expected_datetime = timezone.datetime(3000, 1, 1, 12).astimezone()
-        timezone_mock.now.return_value = expected_datetime
-        seal(timezone_mock)
+    def test_partial_update_final_task_state_pending(self):
+        self.task1.state = State.Completed
+        self.task1.save()
+        self.client.force_login(self.superuser)
 
-        self.process.finished = None
-        self.process.name = "ok"
-        self.process.save()
-        self.process.tasks.filter(id=self.task2.id).update(state=State.Completed, run=2)
-        self.process.tasks.filter(id=self.task3.id).update(agent=self.agent, state=State.Running, run=2)
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(5):
             resp = self.client.patch(
-                reverse("api:task-details", args=[self.task3.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+                reverse("api:task-update", args=[self.task1.id]),
+                data={"state": State.Pending.value},
             )
             self.assertEqual(resp.status_code, status.HTTP_200_OK)
 
-        self.assertEqual(len(mail.outbox), 1)
-        message = mail.outbox[0]
-        self.assertEqual(message.subject, "Your process ok finished successfully")
-        self.assertListEqual(message.to, [self.process.creator.email])
-        self.assertEqual(message.body, dedent(f"""
-        Hello Test user,
-
-        A process you created just finished on run 2.
-        ID: {self.process.id}
-        Overall state: Completed
+        self.task1.refresh_from_db()
+        self.assertEqual(self.task1.state, State.Pending)
+        self.assertIsNone(self.task1.agent)
 
-        Tasks summary:
-        * {self.task2.slug}: Completed
-        * {self.task3.slug}: Completed
+    def test_partial_update_non_final_task_state_pending(self):
+        states = set(State) - set(FINAL_STATES)
+        self.task1.save()
+        self.client.force_login(self.superuser)
 
-        You can see all the details and logs here: http://ark.teklia/process/{self.process.id}/2
+        for state in states:
+            with self.subTest(state=state):
+                self.task1.state = state
+                self.task1.save()
 
-        --
-        Arkindex
+                with self.assertNumQueries(3):
+                    resp = self.client.patch(
+                        reverse("api:task-update", args=[self.task1.id]),
+                        data={"state": State.Pending.value},
+                    )
+                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
 
-        """))
+                self.assertDictEqual(
+                    resp.json(),
+                    {"state": [f"Transition from state {state} to state Pending is forbidden."]}
+                )
+                self.task1.refresh_from_db()
+                self.assertEqual(self.task1.state, state)
 
-    @patch("arkindex.ponos.signals.task_failure.send_robust")
     @patch("arkindex.project.aws.s3")
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    def test_task_final_not_completed_triggers_signal(
-        self, async_notify, s3_mock, send_mock
-    ):
+    def test_task_logs_unicode_error(self, s3_mock):
         """
-        Updating a task to a final state that is not `State.Completed`
-        should trigger the task_failure signal
+        Ensure the TaskLogs.latest property is able to handle sliced off Unicode characters.
+        Since we fetch the latest logs from S3 using `Range: bytes=-N`, sometimes the characters
+        can have a missing byte and cause a UnicodeDecodeError, such as U+00A0 (non-breaking space)
+        or U+00A9 (copyright symbol).
         """
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"Failed successfully")
-        }
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-        seal(s3_mock)
-
-        states_expecting_signal = {State.Error, State.Failed, State.Stopped}
-
-        for state in FINAL_STATES:
-            if state is State.Completed:
-                continue
-            with self.subTest(state=state):
-                if state is State.Stopped:
-                    self.task1.state = State.Stopping
-                else:
-                    self.task1.state = State.Running
-                self.task1.agent = self.agent
-                self.task1.save()
-
-                resp = self.client.patch(
-                    reverse("api:task-details", args=[self.task1.id]),
-                    data={"state": state.value},
-                    HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-                )
-                self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-                if state in states_expecting_signal:
-                    self.assertEqual(send_mock.call_count, 1)
-                    _, kwargs = send_mock.call_args
-                    self.assertDictEqual(kwargs, {"task": self.task1})
-                else:
-                    self.assertFalse(send_mock.called)
-
-                send_mock.reset_mock()
+        with self.assertRaises(UnicodeDecodeError):
+            b"\xa0Failed successfully".decode("utf-8")
 
-    @patch("django.contrib.auth")
-    @patch("arkindex.project.aws.s3")
-    def test_task_update_gpu(self, s3_mock, auth_mock):
-        auth_mock.signals.user_logged_in = None
         s3_mock.Object.return_value.bucket_name = "ponos"
         s3_mock.Object.return_value.key = "somelog"
         s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"Some nice GPU task output...")
+            "Body": BytesIO(b"\xa0Failed successfully")
         }
         s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-        seal(s3_mock)
-
-        # All GPUs are available
-        self.assertFalse(GPU.objects.filter(tasks__isnull=False).exists())
-
-        # The task is running on a GPU
-        self.task1.gpu = self.agent.gpus.first()
-        self.task1.agent = self.agent
-        self.task1.state = State.Running
-        self.task1.save()
-
-        # One GPU is assigned
-        self.assertTrue(GPU.objects.filter(tasks__isnull=False).exists())
 
+        self.client.force_login(self.superuser)
         with self.assertNumQueries(4):
-            resp = self.client.get(
-                reverse("api:task-details", args=[self.task1.id]),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        # Last ping is updated in the API call
-        self.agent.refresh_from_db()
-
-        self.assertDictEqual(
-            resp.json(),
-            {
-                "id": str(self.task1.id),
-                "run": 0,
-                "depth": 0,
-                "extra_files": {},
-                "slug": "initialisation",
-                "state": "running",
-                "parents": [],
-                "shm_size": None,
-                "logs": "Some nice GPU task output...",
-                "full_log": "http://somewhere",
-                "agent": {
-                    "cpu_cores": 2,
-                    "cpu_frequency": 1000000000,
-                    "cpu_load": .1,
-                    "farm": {"id": str(self.agent.farm_id), "name": "Wheat farm"},
-                    "gpus": [
-                        {
-                            "id": "108c6524-c63a-4811-bbed-9723d32a0688",
-                            "index": 0,
-                            "name": "GPU1",
-                            "ram_total": 2147483648,
-                        },
-                        {
-                            "id": "f30d1407-92bb-484b-84b0-0b8bae41ca91",
-                            "index": 1,
-                            "name": "GPU2",
-                            "ram_total": 8589934592,
-                        },
-                    ],
-                    "hostname": "ghostname",
-                    "id": str(self.agent.id),
-                    "last_ping": str_date(self.agent.last_ping),
-                    "ram_load": 100000000,
-                    "ram_total": 2000000000,
-                },
-                "gpu": {
-                    "id": "108c6524-c63a-4811-bbed-9723d32a0688",
-                    "index": 0,
-                    "name": "GPU1",
-                    "ram_total": 2147483648,
-                },
-            },
-        )
-
-        # Now let's complete that task, and check that the GPU is still assigned
-        with self.assertNumQueries(9):
-            resp = self.client.patch(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": "completed"},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.assertDictEqual(
-            resp.json(),
-            {
-                "id": str(self.task1.id),
-                "run": 0,
-                "depth": 0,
-                "extra_files": {},
-                "slug": "initialisation",
-                "state": "completed",
-                "parents": [],
-                "shm_size": None,
-                "logs": "",
-                "full_log": "http://somewhere",
-                "agent": {
-                    "cpu_cores": 2,
-                    "cpu_frequency": 1000000000,
-                    "cpu_load": .1,
-                    "farm": {"id": str(self.agent.farm_id), "name": "Wheat farm"},
-                    "gpus": [
-                        {
-                            "id": "108c6524-c63a-4811-bbed-9723d32a0688",
-                            "index": 0,
-                            "name": "GPU1",
-                            "ram_total": 2147483648,
-                        },
-                        {
-                            "id": "f30d1407-92bb-484b-84b0-0b8bae41ca91",
-                            "index": 1,
-                            "name": "GPU2",
-                            "ram_total": 8589934592,
-                        },
-                    ],
-                    "hostname": "ghostname",
-                    "id": str(self.agent.id),
-                    "last_ping": str_date(self.agent.last_ping),
-                    "ram_load": 100000000,
-                    "ram_total": 2000000000,
-                },
-                "gpu": {
-                    "id": "108c6524-c63a-4811-bbed-9723d32a0688",
-                    "index": 0,
-                    "name": "GPU1",
-                    "ram_total": 2147483648,
-                },
-            },
-        )
-
-        # All GPUs are available: no GPU has any task in an active state
-        self.assertFalse(GPU.objects.filter(tasks__state__in=ACTIVE_STATES).exists())
-
-    def test_update_task_from_agent_requires_login(self):
-        with self.assertNumQueries(0):
-            resp = self.client.put(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_update_task_from_agent_forbids_users(self):
-        self.client.force_login(self.user)
-        with self.assertNumQueries(2):
-            resp = self.client.put(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_update_task_from_agent_forbids_task(self):
-        with self.assertNumQueries(1):
-            resp = self.client.put(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_update_task_from_agent_unassigned(self):
-        self.assertNotEqual(self.task1.agent, self.agent)
-
-        with self.assertNumQueries(2):
-            resp = self.client.put(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    @patch("arkindex.ponos.models.TaskLogs.latest", new_callable=PropertyMock)
-    def test_update_task_from_agent(self, short_logs_mock):
-        short_logs_mock.return_value = ""
-        self.process.tasks.filter(id=self.task1.id).update(state=State.Pending, agent=self.agent)
-        with self.assertNumQueries(5):
-            resp = self.client.put(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Running.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
+            resp = self.client.get(reverse("api:task-details", args=[self.task1.id]))
             self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
         data = resp.json()
-        self.assertEqual(data["id"], str(self.task1.id))
-        self.assertEqual(data["state"], State.Running.value)
-        self.task1.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Running)
 
-    @patch("arkindex.ponos.models.TaskLogs.latest", new_callable=PropertyMock)
-    def test_update_task_from_agent_completed_pends_children(self, short_logs_mock):
+        self.assertEqual(data["logs"], "�Failed successfully")
+
+    @patch("arkindex.project.aws.s3")
+    def test_task_logs_errors(self, s3_mock):
         """
-        Child tasks whose parents are all completed get set to pending as soon as the agent updates the last parent
+        Ensure the TaskLogs.latest property handles missing log files and returns an empty string,
+        or handles an InvalidRange error returned only by Ceph when the log file exists but has zero bytes.
         """
-        short_logs_mock.return_value = ""
+        s3_mock.Object.return_value.bucket_name = "ponos"
+        s3_mock.Object.return_value.key = "somelog"
+        s3_mock.Object.return_value.get.side_effect = ClientError({"Error": {"Code": ""}}, "get_object")
+        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
+        seal(s3_mock)
 
-        self.task1.agent = self.agent
-        self.task2.agent = self.agent
-        self.task1.state = State.Running
-        self.task2.state = State.Running
-        self.task1.depth = 0
-        self.task2.depth = 0
-        self.task3.depth = 1
-        self.task1.save()
-        self.task2.save()
-        self.task3.save()
-        self.task1.parents.clear()
-        self.task2.parents.clear()
-        self.task3.parents.set([self.task1, self.task2])
+        self.client.force_login(self.superuser)
 
-        with self.assertNumQueries(9):
-            resp = self.client.put(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
+        for code in ["NoSuchKey", "InvalidRange"]:
+            with self.subTest(code=code), self.assertNumQueries(4):
+                s3_mock.Object.return_value.get.side_effect.response["Error"]["Code"] = str(code)
 
-        self.task1.refresh_from_db()
-        self.task2.refresh_from_db()
-        self.task3.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Completed)
-        self.assertEqual(self.task2.state, State.Running)
-        # Only one of the two parents is completed: nothing happens.
-        self.assertEqual(self.task3.state, State.Unscheduled)
-
-        with self.assertNumQueries(9):
-            resp = self.client.put(
-                reverse("api:task-details", args=[self.task2.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.task1.refresh_from_db()
-        self.task2.refresh_from_db()
-        self.task3.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Completed)
-        self.assertEqual(self.task2.state, State.Completed)
-        # Both parents are completed, the child task is now pending
-        self.assertEqual(self.task3.state, State.Pending)
-
-    def test_update_task_requires_login(self):
-        with self.assertNumQueries(0):
-            resp = self.client.put(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_update_task_requires_verified(self):
-        self.user.verified_email = False
-        self.user.save()
-        self.client.force_login(self.user)
-
-        with self.assertNumQueries(2):
-            resp = self.client.put(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_update_task_forbids_agent(self):
-        with self.assertNumQueries(0):
-            resp = self.client.put(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_update_task_forbids_task(self):
-        with self.assertNumQueries(0):
-            resp = self.client.put(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_update_task_requires_process_admin_corpus(self):
-        self.process.creator = self.superuser
-        self.process.save()
-        self.corpus.public = False
-        self.corpus.save()
-        self.client.force_login(self.user)
-
-        for role in [None, Role.Guest, Role.Contributor]:
-            with self.subTest(role=role):
-                self.corpus.memberships.filter(user=self.user).delete()
-                if role:
-                    self.corpus.memberships.create(user=self.user, level=role.value)
-
-                with self.assertNumQueries(5):
-                    resp = self.client.put(
-                        reverse("api:task-update", args=[self.task1.id]),
-                        data={"state": State.Stopping.value},
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_update_task_requires_process_admin_repo(self):
-        self.process.mode = ProcessMode.Repository
-        self.process.corpus = None
-        self.process.revision = self.rev
-        self.process.creator = self.superuser
-        self.process.save()
-        self.client.force_login(self.user)
-
-        for role in [None, Role.Guest, Role.Contributor]:
-            with self.subTest(role=role):
-                self.rev.repo.memberships.filter(user=self.user).delete()
-                if role:
-                    self.rev.repo.memberships.create(user=self.user, level=role.value)
-
-                with self.assertNumQueries(5):
-                    resp = self.client.put(
-                        reverse("api:task-update", args=[self.task1.id]),
-                        data={"state": State.Stopping.value},
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_update_running_task_state_stopping(self):
-        self.task1.state = State.Running
-        self.task1.save()
-        self.client.force_login(self.superuser)
-
-        with self.assertNumQueries(4):
-            resp = self.client.put(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.assertDictEqual(resp.json(), {
-            "id": str(self.task1.id),
-            "state": State.Stopping.value,
-        })
-        self.task1.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Stopping)
-
-    def test_update_non_running_task_state_stopping(self):
-        states = list(State)
-        states.remove(State.Running)
-        self.client.force_login(self.superuser)
-
-        for state in states:
-            with self.subTest(state=state):
-                self.task1.state = state
-                self.task1.save()
-
-                with self.assertNumQueries(3):
-                    resp = self.client.put(
-                        reverse("api:task-update", args=[self.task1.id]),
-                        data={"state": State.Stopping.value},
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-                self.assertDictEqual(
-                    resp.json(),
-                    {"state": [f"Transition from state {state} to state Stopping is forbidden."]},
-                )
-
-                self.task1.refresh_from_db()
-                self.assertEqual(self.task1.state, state)
-
-    def test_update_final_task_state_pending(self):
-        self.task1.state = State.Completed
-        self.task1.agent = self.agent
-        self.task1.gpu = self.agent.gpus.first()
-        self.task1.save()
-        self.client.force_login(self.superuser)
-
-        with self.assertNumQueries(5):
-            resp = self.client.put(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Pending.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.task1.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Pending)
-        self.assertIsNone(self.task1.agent)
-        self.assertIsNone(self.task1.gpu)
-
-    def test_update_non_final_task_state_pending(self):
-        states = set(State) - set(FINAL_STATES)
-        self.client.force_login(self.superuser)
-
-        for state in states:
-            with self.subTest(state=state):
-                self.task1.state = state
-                self.task1.save()
-
-                with self.assertNumQueries(3):
-                    resp = self.client.put(
-                        reverse("api:task-update", args=[self.task1.id]),
-                        data={"state": State.Pending.value},
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-
-                self.assertDictEqual(
-                    resp.json(),
-                    {"state": [f"Transition from state {state} to state Pending is forbidden."]}
-                )
-                self.task1.refresh_from_db()
-                self.assertEqual(self.task1.state, state)
-
-    @patch("arkindex.ponos.models.TaskLogs.latest", new_callable=PropertyMock)
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    def test_partial_update_task_from_agent_allowed_states(self, notify_mock, short_logs_mock):
-        short_logs_mock.return_value = ""
-        self.task1.agent = self.agent
-        self.task1.save()
-
-        cases = [
-            (State.Unscheduled, State.Pending, 5),
-            (State.Pending, State.Running, 5),
-            (State.Pending, State.Error, 11),
-            (State.Running, State.Completed, 9),
-            (State.Running, State.Failed, 9),
-            (State.Running, State.Error, 9),
-            (State.Stopping, State.Stopped, 9),
-            (State.Stopping, State.Error, 9),
-        ]
-
-        for from_state, to_state, query_count in cases:
-            with self.subTest(from_state=from_state, to_state=to_state):
-                self.task1.state = from_state
-                self.task1.save()
-
-                with self.assertNumQueries(query_count):
-                    resp = self.client.patch(
-                        reverse("api:task-details", args=[self.task1.id]),
-                        data={"state": to_state.value},
-                        HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-                data = resp.json()
-                self.assertEqual(data["id"], str(self.task1.id))
-                self.assertEqual(data["state"], to_state.value)
-                self.task1.refresh_from_db()
-                self.assertEqual(self.task1.state, to_state)
-
-    def test_partial_update_task_from_agent_forbidden_states(self):
-        self.task1.agent = self.agent
-        self.task1.save()
-
-        cases = [
-            (State.Unscheduled, State.Running),
-            (State.Unscheduled, State.Completed),
-            (State.Unscheduled, State.Failed),
-            (State.Unscheduled, State.Error),
-            (State.Unscheduled, State.Stopping),
-            (State.Unscheduled, State.Stopped),
-            (State.Pending, State.Unscheduled),
-            (State.Pending, State.Completed),
-            (State.Pending, State.Failed),
-            (State.Pending, State.Stopping),
-            (State.Pending, State.Stopped),
-            (State.Running, State.Unscheduled),
-            (State.Running, State.Pending),
-            (State.Running, State.Stopping),
-            (State.Running, State.Stopped),
-            (State.Stopping, State.Unscheduled),
-            (State.Stopping, State.Pending),
-            (State.Stopping, State.Running),
-            (State.Stopping, State.Completed),
-            (State.Stopping, State.Failed),
-            # Cannot go from one state to the same state
-            *((state, state) for state in State),
-            # Cannot go from a final state to anywhere
-            *((final_state, state) for final_state in FINAL_STATES for state in State),
-        ]
-
-        for from_state, to_state in cases:
-            with self.subTest(from_state=from_state, to_state=to_state):
-                self.task1.state = from_state
-                self.task1.save()
-
-                with self.assertNumQueries(2):
-                    resp = self.client.put(
-                        reverse("api:task-details", args=[self.task1.id]),
-                        data={"state": to_state.value},
-                        HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-
-                self.assertDictEqual(
-                    resp.json(),
-                    {"state": [f"Transition from state {from_state} to state {to_state} is forbidden."]},
-                )
-
-                self.task1.refresh_from_db()
-                self.assertEqual(self.task1.state, from_state)
-
-    def test_partial_update_task_from_agent_requires_login(self):
-        with self.assertNumQueries(0):
-            resp = self.client.patch(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_partial_update_task_from_agent_forbids_users(self):
-        self.client.force_login(self.user)
-        with self.assertNumQueries(2):
-            resp = self.client.patch(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_partial_update_task_from_agent_forbids_task(self):
-        with self.assertNumQueries(1):
-            resp = self.client.patch(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_partial_update_task_from_agent_unassigned(self):
-        self.assertNotEqual(self.task1.agent, self.agent)
-
-        with self.assertNumQueries(2):
-            resp = self.client.patch(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    @patch("arkindex.ponos.models.TaskLogs.latest", new_callable=PropertyMock)
-    def test_partial_update_task_from_agent_completed_pends_children(self, short_logs_mock):
-        """
-        Child tasks whose parents are all completed get set to pending as soon as the agent updates the last parent
-        """
-        short_logs_mock.return_value = ""
-
-        self.task1.agent = self.agent
-        self.task2.agent = self.agent
-        self.task1.depth = 0
-        self.task2.depth = 0
-        self.task3.depth = 1
-        self.task1.state = State.Running
-        self.task2.state = State.Running
-        self.task3.state = State.Unscheduled
-        self.task1.save()
-        self.task2.save()
-        self.task3.save()
-        self.task1.parents.clear()
-        self.task2.parents.clear()
-        self.task3.parents.set([self.task1, self.task2])
-
-        with self.assertNumQueries(9):
-            resp = self.client.patch(
-                reverse("api:task-details", args=[self.task1.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.task1.refresh_from_db()
-        self.task2.refresh_from_db()
-        self.task3.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Completed)
-        self.assertEqual(self.task2.state, State.Running)
-        # Only one of the two parents is completed: nothing happens.
-        self.assertEqual(self.task3.state, State.Unscheduled)
-        self.assertIsNone(self.process.finished)
-
-        with self.assertNumQueries(9):
-            resp = self.client.patch(
-                reverse("api:task-details", args=[self.task2.id]),
-                data={"state": State.Completed.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.task1.refresh_from_db()
-        self.task2.refresh_from_db()
-        self.task3.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Completed)
-        self.assertEqual(self.task2.state, State.Completed)
-        # Both parents are completed, the child task is now pending
-        self.assertEqual(self.task3.state, State.Pending)
-        self.assertIsNone(self.process.finished)
-
-    def test_partial_update_task_requires_login(self):
-        with self.assertNumQueries(0):
-            resp = self.client.patch(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_partial_update_task_requires_verified(self):
-        self.user.verified_email = False
-        self.user.save()
-        self.client.force_login(self.user)
-
-        with self.assertNumQueries(2):
-            resp = self.client.patch(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_partial_update_task_forbids_agent(self):
-        with self.assertNumQueries(0):
-            resp = self.client.patch(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_partial_update_task_forbids_task(self):
-        with self.assertNumQueries(0):
-            resp = self.client.patch(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_partial_update_task_requires_process_admin_corpus(self):
-        self.process.creator = self.superuser
-        self.process.save()
-        self.corpus.public = False
-        self.corpus.save()
-        self.client.force_login(self.user)
-
-        for role in [None, Role.Guest, Role.Contributor]:
-            with self.subTest(role=role):
-                self.corpus.memberships.filter(user=self.user).delete()
-                if role:
-                    self.corpus.memberships.create(user=self.user, level=role.value)
-
-                with self.assertNumQueries(5):
-                    resp = self.client.patch(
-                        reverse("api:task-update", args=[self.task1.id]),
-                        data={"state": State.Stopping.value},
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_partial_update_task_requires_process_admin_repo(self):
-        self.process.mode = ProcessMode.Repository
-        self.process.corpus = None
-        self.process.revision = self.rev
-        self.process.creator = self.superuser
-        self.process.save()
-        self.client.force_login(self.user)
-
-        for role in [None, Role.Guest, Role.Contributor]:
-            with self.subTest(role=role):
-                self.rev.repo.memberships.filter(user=self.user).delete()
-                if role:
-                    self.rev.repo.memberships.create(user=self.user, level=role.value)
-
-                with self.assertNumQueries(5):
-                    resp = self.client.patch(
-                        reverse("api:task-update", args=[self.task1.id]),
-                        data={"state": State.Stopping.value},
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_partial_update_running_task_state_stopping(self):
-        self.task1.state = State.Running
-        self.task1.save()
-        self.client.force_login(self.superuser)
-
-        with self.assertNumQueries(4):
-            resp = self.client.patch(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Stopping.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.assertDictEqual(resp.json(), {
-            "id": str(self.task1.id),
-            "state": State.Stopping.value,
-        })
-        self.task1.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Stopping)
-
-    def test_partial_update_non_running_task_state_stopping(self):
-        states = list(State)
-        states.remove(State.Running)
-        self.task1.save()
-        self.client.force_login(self.superuser)
-
-        for state in states:
-            with self.subTest(state=state):
-                self.task1.state = state
-                self.task1.save()
-
-                with self.assertNumQueries(3):
-                    resp = self.client.patch(
-                        reverse("api:task-update", args=[self.task1.id]),
-                        data={"state": State.Stopping.value},
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-                self.assertDictEqual(
-                    resp.json(),
-                    {"state": [f"Transition from state {state} to state Stopping is forbidden."]}
-                )
-
-                self.task1.refresh_from_db()
-                self.assertEqual(self.task1.state, state)
-
-    def test_partial_update_final_task_state_pending(self):
-        self.task1.state = State.Completed
-        self.task1.agent = self.agent
-        self.task1.save()
-        self.client.force_login(self.superuser)
-
-        with self.assertNumQueries(5):
-            resp = self.client.patch(
-                reverse("api:task-update", args=[self.task1.id]),
-                data={"state": State.Pending.value},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.task1.refresh_from_db()
-        self.assertEqual(self.task1.state, State.Pending)
-        self.assertIsNone(self.task1.agent)
-
-    def test_partial_update_non_final_task_state_pending(self):
-        states = set(State) - set(FINAL_STATES)
-        self.task1.save()
-        self.client.force_login(self.superuser)
-
-        for state in states:
-            with self.subTest(state=state):
-                self.task1.state = state
-                self.task1.save()
-
-                with self.assertNumQueries(3):
-                    resp = self.client.patch(
-                        reverse("api:task-update", args=[self.task1.id]),
-                        data={"state": State.Pending.value},
-                    )
-                    self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-
-                self.assertDictEqual(
-                    resp.json(),
-                    {"state": [f"Transition from state {state} to state Pending is forbidden."]}
-                )
-                self.task1.refresh_from_db()
-                self.assertEqual(self.task1.state, state)
-
-    @patch("arkindex.project.aws.s3")
-    def test_task_logs_unicode_error(self, s3_mock):
-        """
-        Ensure the TaskLogs.latest property is able to handle sliced off Unicode characters.
-        Since we fetch the latest logs from S3 using `Range: bytes=-N`, sometimes the characters
-        can have a missing byte and cause a UnicodeDecodeError, such as U+00A0 (non-breaking space)
-        or U+00A9 (copyright symbol).
-        """
-        with self.assertRaises(UnicodeDecodeError):
-            b"\xa0Failed successfully".decode("utf-8")
-
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.return_value = {
-            "Body": BytesIO(b"\xa0Failed successfully")
-        }
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(4):
-            resp = self.client.get(reverse("api:task-details", args=[self.task1.id]))
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-        data = resp.json()
-
-        self.assertEqual(data["logs"], "�Failed successfully")
-
-    @patch("arkindex.project.aws.s3")
-    def test_task_logs_errors(self, s3_mock):
-        """
-        Ensure the TaskLogs.latest property handles missing log files and returns an empty string,
-        or handles an InvalidRange error returned only by Ceph when the log file exists but has zero bytes.
-        """
-        s3_mock.Object.return_value.bucket_name = "ponos"
-        s3_mock.Object.return_value.key = "somelog"
-        s3_mock.Object.return_value.get.side_effect = ClientError({"Error": {"Code": ""}}, "get_object")
-        s3_mock.meta.client.generate_presigned_url.return_value = "http://somewhere"
-        seal(s3_mock)
-
-        self.client.force_login(self.superuser)
-
-        for code in ["NoSuchKey", "InvalidRange"]:
-            with self.subTest(code=code), self.assertNumQueries(4):
-                s3_mock.Object.return_value.get.side_effect.response["Error"]["Code"] = str(code)
-
-                resp = self.client.get(reverse("api:task-details", args=[self.task1.id]))
-                self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-                self.assertEqual(resp.json()["logs"], "")
-
-    @patch("arkindex.ponos.serializers.check_agent_key")
-    @patch("arkindex.ponos.serializers.timezone")
-    def test_agent_create(self, timezone_mock, check_mock):
-        check_mock.return_value = True
-        timezone_mock.now.return_value = timezone.datetime(2000, 1, 1, 12).astimezone()
-
-        resp = self.client.post(
-            reverse("api:agent-register"),
-            {
-                "hostname": "toastname",
-                "cpu_cores": 42,
-                "cpu_frequency": 1337e6,
-                "ram_total": 16e9,
-                "farm": str(self.wheat_farm.id),
-                "public_key": build_public_key(),
-                "derivation": "{:032x}".format(random.getrandbits(128)),
-                "gpus": [
-                    {
-                        "id": "b6b8d7c1-c6bd-4de6-ae92-866a270be36f",
-                        "name": "A",
-                        "index": 0,
-                        "ram_total": 512 * 1024 * 1024,
-                    },
-                    {
-                        "id": "14fe053a-8014-46a3-ad7b-99fa4389d74c",
-                        "name": "B",
-                        "index": 1,
-                        "ram_total": 2 * 1024 * 1024 * 1024,
-                    },
-                ],
-            },
-            format="json",
-        )
-
-        self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(check_mock.call_count, 1)
-        data = resp.json()
-        agent = Agent.objects.get(id=data["id"])
-        self.assertEqual(agent.hostname, "toastname")
-        self.assertEqual(agent.cpu_cores, 42)
-        self.assertEqual(agent.cpu_frequency, 1337e6)
-        self.assertEqual(agent.gpus.count(), 2)
-        gpu_a = agent.gpus.get(name="A")
-        gpu_b = agent.gpus.get(name="B")
-        self.assertEqual(gpu_a.index, 0)
-        self.assertEqual(gpu_b.index, 1)
-        self.assertEqual(gpu_a.ram_total, 512 * 1024 * 1024)
-        self.assertEqual(gpu_b.ram_total, 2 * 1024 * 1024 * 1024)
-
-        self.assertIn("access_token", data)
-        self.assertIn("refresh_token", data)
-        del data["access_token"]
-        del data["refresh_token"]
-
-        self.assertDictEqual(
-            data,
-            {
-                "id": str(agent.id),
-                "hostname": "toastname",
-                "cpu_cores": 42,
-                "cpu_frequency": int(1337e6),
-                "farm": str(self.wheat_farm.id),
-                "ram_total": 16_000_000_000,
-                "cpu_load": None,
-                "ram_load": None,
-                "last_ping": "2000-01-01T12:00:00Z",
-                "gpus": [
-                    {
-                        "id": "b6b8d7c1-c6bd-4de6-ae92-866a270be36f",
-                        "index": 0,
-                        "name": "A",
-                        "ram_total": 512 * 1024 * 1024,
-                    },
-                    {
-                        "id": "14fe053a-8014-46a3-ad7b-99fa4389d74c",
-                        "index": 1,
-                        "name": "B",
-                        "ram_total": 2 * 1024 * 1024 * 1024,
-                    },
-                ],
-            },
-        )
-
-    @patch("arkindex.ponos.serializers.check_agent_key")
-    @patch("arkindex.ponos.serializers.timezone")
-    def test_agent_update(self, timezone_mock, check_mock):
-        check_mock.return_value = True
-        timezone_mock.now.return_value = timezone.datetime(2000, 1, 1, 12).astimezone()
-
-        # Still a POST, but on an existing agent
-        resp = self.client.post(
-            reverse("api:agent-register"),
-            {
-                "hostname": self.agent.hostname,
-                "cpu_cores": 12,
-                "cpu_frequency": 1e9,
-                "ram_total": 32e9,
-                "farm": str(self.wheat_farm.id),
-                "public_key": self.agent.public_key,
-                "derivation": "{:032x}".format(random.getrandbits(128)),
-                "gpus": [
-                    {
-                        "id": "deadbeef-c6bd-4de6-ae92-866a270be36f",
-                        "name": "new gpu",
-                        "index": 0,
-                        "ram_total": 32 * 1024 * 1024 * 1024,
-                    },
-                ],
-            },
-            format="json",
-        )
-
-        self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(check_mock.call_count, 1)
-        data = resp.json()
-        self.agent.refresh_from_db()
-        self.assertEqual(self.agent.hostname, "ghostname")
-        self.assertEqual(self.agent.cpu_cores, 12)
-        self.assertEqual(self.agent.cpu_frequency, 1e9)
-        self.assertEqual(self.agent.gpus.count(), 1)
-        gpu = self.agent.gpus.get(name="new gpu")
-        self.assertEqual(gpu.index, 0)
-        self.assertEqual(gpu.ram_total, 32 * 1024 * 1024 * 1024)
-        self.assertEqual(gpu.id, uuid.UUID("deadbeef-c6bd-4de6-ae92-866a270be36f"))
-
-        self.assertIn("access_token", data)
-        self.assertIn("refresh_token", data)
-        del data["access_token"]
-        del data["refresh_token"]
-
-        self.assertDictEqual(
-            data,
-            {
-                "cpu_cores": 12,
-                "cpu_frequency": 1000000000,
-                "cpu_load": .1,
-                "farm": str(self.wheat_farm.id),
-                "gpus": [
-                    {
-                        "id": "deadbeef-c6bd-4de6-ae92-866a270be36f",
-                        "index": 0,
-                        "name": "new gpu",
-                        "ram_total": 34359738368,
-                    }
-                ],
-                "hostname": "ghostname",
-                "id": str(self.agent.id),
-                "last_ping": "2000-01-01T12:00:00Z",
-                "ram_load": 100000000,
-                "ram_total": 32000000000,
-            },
-        )
-
-    @patch("arkindex.ponos.serializers.check_agent_key")
-    @patch("arkindex.ponos.serializers.timezone")
-    def test_agent_create_existing_gpu(self, timezone_mock, check_mock):
-        check_mock.return_value = True
-        timezone_mock.now.return_value = timezone.datetime(2000, 1, 1, 12).astimezone()
-
-        resp = self.client.post(
-            reverse("api:agent-register"),
-            {
-                "hostname": "toastname",
-                "cpu_cores": 42,
-                "cpu_frequency": 1337e6,
-                "ram_total": 16e9,
-                "farm": str(self.wheat_farm.id),
-                "public_key": build_public_key(),
-                "derivation": "{:032x}".format(random.getrandbits(128)),
-                "gpus": [
-                    {
-                        "id": str(self.gpu1.id),
-                        "name": "A",
-                        "index": 0,
-                        "ram_total": 512 * 1024 * 1024,
-                    },
-                    {
-                        "id": str(self.gpu2.id),
-                        "name": "B",
-                        "index": 1,
-                        "ram_total": 2 * 1024 * 1024 * 1024,
-                    },
-                ],
-            },
-            format="json",
-        )
-        self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(check_mock.call_count, 1)
-        data = resp.json()
-
-        self.gpu1.refresh_from_db()
-        self.gpu2.refresh_from_db()
-        self.assertEqual(self.gpu1.name, "A")
-        self.assertEqual(self.gpu2.name, "B")
-        self.assertEqual(self.gpu1.ram_total, 512 * 1024**2)
-        self.assertEqual(self.gpu2.ram_total, 2 * 1024**3)
-
-        new_agent = Agent.objects.get(id=data["id"])
-        self.assertEqual(self.gpu1.agent, new_agent)
-        self.assertEqual(self.gpu2.agent, new_agent)
-
-        # Existing agent no longer has any assigned GPUs, since the new agent stole them
-        self.assertFalse(self.agent.gpus.exists())
-
-    @patch("arkindex.ponos.serializers.check_agent_key")
-    @patch("arkindex.ponos.serializers.timezone")
-    def test_agent_update_existing_gpu(self, timezone_mock, check_mock):
-        check_mock.return_value = True
-        timezone_mock.now.return_value = timezone.datetime(2000, 1, 1, 12).astimezone()
-        self.assertTrue(self.agent.gpus.count(), 2)
-
-        # Still a POST, but on an existing agent
-        resp = self.client.post(
-            reverse("api:agent-register"),
-            {
-                "hostname": self.agent.hostname,
-                "cpu_cores": 12,
-                "cpu_frequency": 1e9,
-                "ram_total": 32e9,
-                "farm": str(self.wheat_farm.id),
-                "public_key": self.agent.public_key,
-                "derivation": "{:032x}".format(random.getrandbits(128)),
-                "gpus": [
-                    {
-                        "id": "108c6524-c63a-4811-bbed-9723d32a0688",
-                        "name": "GPU1",
-                        "index": 0,
-                        "ram_total": 2 * 1024 * 1024 * 1024,
-                    },
-                    {
-                        "id": "f30d1407-92bb-484b-84b0-0b8bae41ca91",
-                        "name": "GPU2",
-                        "index": 1,
-                        "ram_total": 8 * 1024 * 1024 * 1024,
-                    },
-                ],
-            },
-            format="json",
-        )
-
-        self.assertEqual(resp.status_code, status.HTTP_201_CREATED)
-
-        self.agent.refresh_from_db()
-        self.assertTrue(self.agent.gpus.count(), 2)
-
-    @override_settings(PONOS_PRIVATE_KEY=PONOS_PRIVATE_KEY)
-    def test_agent_create_bad_seed(self):
-        # Build keys
-        server_public_key = ec.generate_private_key(
-            ec.SECP384R1(),
-            default_backend(),
-        ).public_key()
-
-        agent_private_key = ec.generate_private_key(
-            ec.SECP384R1(),
-            default_backend(),
-        )
-        agent_public_bytes = agent_private_key.public_key().public_bytes(
-            Encoding.PEM,
-            PublicFormat.SubjectPublicKeyInfo,
-        )
-
-        # Add 1 to the farm's seed
-        wrong_seed = "{:064x}".format(int(self.wheat_farm.seed, 16) + 1)
-
-        # Perform derivation with the wrong seed
-        shared_key = agent_private_key.exchange(ec.ECDH(), server_public_key)
-        derived_key = HKDF(
-            algorithm=SHA256(),
-            backend=default_backend(),
-            length=32,
-            salt=None,
-            info=wrong_seed.encode("utf-8"),
-        ).derive(shared_key)
-
-        resp = self.client.post(
-            reverse("api:agent-register"),
-            {
-                "hostname": "toastname",
-                "cpu_cores": 42,
-                "cpu_frequency": 1337e6,
-                "ram_total": 16e9,
-                "farm": str(self.wheat_farm.id),
-                "public_key": agent_public_bytes.decode("utf-8"),
-                "derivation": base64.b64encode(derived_key).decode("utf-8"),
-                "gpus": [],
-            },
-            format="json",
-        )
-        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            resp.json(), {"non_field_errors": ["Key verification failed"]}
-        )
-
-    def test_agent_actions_requires_token(self):
-        with self.assertNumQueries(0):
-            resp = self.client.get(reverse("api:agent-actions"))
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_agent_actions_no_user(self):
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(0):
-            resp = self.client.get(reverse("api:agent-actions"))
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_agent_actions_no_task(self):
-        with self.assertNumQueries(0):
-            resp = self.client.get(
-                reverse("api:agent-actions"),
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_agent_actions_query_params_required(self):
-        with self.assertNumQueries(1):
-            resp = self.client.get(
-                reverse("api:agent-actions"),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            resp.json(),
-            {
-                "cpu_load": ["This query parameter is required."],
-                "ram_load": ["This query parameter is required."],
-            },
-        )
-
-    def test_agent_actions_query_params_validation(self):
-        with self.assertNumQueries(1):
-            resp = self.client.get(
-                reverse("api:agent-actions"),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-                data={"cpu_load": "high", "ram_load": "low"},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            resp.json(),
-            {
-                "cpu_load": ["A valid number is required."],
-                "ram_load": ["A valid number is required."],
-            },
-        )
-
-    def test_agent_null_state(self):
-        """
-        Agents with unknown CPU or RAM load are excluded by the assignation algorithm
-        """
-        self.agent.cpu_load = None
-        self.agent.ram_load = None
-        self.agent.save()
-        pubkey = build_public_key()
-        second_agent = AgentUser.objects.create(
-            id=hashlib.md5(pubkey.encode("utf-8")).hexdigest(),
-            farm=self.wheat_farm,
-            hostname="new agent",
-            cpu_cores=2,
-            cpu_frequency=1e9,
-            public_key=pubkey,
-            ram_total=1e9,
-            last_ping=timezone.now(),
-        )
-        with self.assertNumQueries(6):
-            resp = self.client.get(
-                reverse("api:agent-actions"),
-                HTTP_AUTHORIZATION=f"Bearer {second_agent.token.access_token}",
-                data={"cpu_load": 1.9, "ram_load": 0.49},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(resp.json(), {"actions": []})
-
-    def test_agent_non_pending_actions(self):
-        """
-        Only pending tasks may be retrieved as new actions
-        """
-        Task.objects.filter(process__farm=self.agent.farm_id).update(state=State.Error)
-        with self.assertNumQueries(7):
-            resp = self.client.get(
-                reverse("api:agent-actions"),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-                data={"cpu_load": 0.9, "ram_load": 0.49},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(resp.json(), {"actions": []})
-
-    def test_agent_no_stealing(self):
-        """
-        An agent may not take another agent's tasks
-        """
-        Task.objects.filter(process__farm=self.agent.farm_id).update(agent=self.agent, state=State.Pending)
-        pubkey = build_public_key()
-        agent2 = AgentUser.objects.create(
-            id=uuid.UUID(hashlib.md5(pubkey.encode("utf-8")).hexdigest()),
-            farm=self.wheat_farm,
-            hostname="agentorange",
-            cpu_cores=2,
-            cpu_frequency=1e9,
-            public_key=pubkey,
-            ram_total=2e9,
-            last_ping=timezone.now(),
-            cpu_load=.007,
-        )
-        with self.assertNumQueries(7):
-            resp = self.client.get(
-                reverse("api:agent-actions"),
-                HTTP_AUTHORIZATION=f"Bearer {agent2.token.access_token}",
-                data={"cpu_load": 0.9, "ram_load": 0.49},
-            )
-            self.assertEqual(resp.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(resp.json(), {"actions": []})
-
-    def test_agent_actions(self):
-        """
-        Agent may retrieve one task using the API due to its resources limitations
-        """
-        self.process.farm = self.agent.farm
-        self.process.save()
-        self.process.tasks.update(state=State.Pending)
-        now = timezone.now()
-
-        with patch.object(api_tz, "now") as api_now_mock:
-            api_now_mock.return_value = now
-            with self.assertNumQueries(11):
-                resp = self.client.get(
-                    reverse("api:agent-actions"),
-                    HTTP_AUTHORIZATION="Bearer {}".format(
-                        self.agent.token.access_token
-                    ),
-                    data={"cpu_load": 0.9, "ram_load": 0.49},
-                )
-        self.assertEqual(resp.status_code, status.HTTP_200_OK)
-
-        self.agent.refresh_from_db()
-        self.assertEqual(self.agent._estimate_new_tasks_cost(), 0.99)
-
-        self.assertDictEqual(
-            resp.json(),
-            {
-                "actions": [
-                    {
-                        "action": "start_task",
-                        "task_id": str(self.task1.id),
-                    }
-                ]
-            },
-        )
-        # Agent load and last ping attributes have been updated
-        self.assertEqual(
-            (self.agent.cpu_load, self.agent.ram_load, self.agent.last_ping),
-            (0.9, 0.49, now),
-        )
-
-    def test_task_create_requires_task_auth(self):
-        response = self.client.post(reverse("api:task-create"))
-        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-        self.assertDictEqual(
-            response.json(),
-            {"detail": "Authentication credentials were not provided."}
-        )
-
-    def test_task_create_user_forbidden(self):
-        self.client.force_login(self.user)
-        response = self.client.post(reverse("api:task-create"))
-        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-        self.assertDictEqual(
-            response.json(),
-            {"detail": "Authentication credentials were not provided."}
-        )
-
-    def test_task_create_agent_forbidden(self):
-        self.client.force_login(self.user)
-        response = self.client.post(reverse("api:task-create"), HTTP_AUTHORIZATION=f"Bearer {self.agent.token}")
-        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-        self.assertDictEqual(
-            response.json(),
-            {"detail": "Authentication credentials were not provided."}
-        )
-
-    def test_task_create_empty_body(self):
-        response = self.client.post(reverse("api:task-create"), HTTP_AUTHORIZATION=f"Ponos {self.task1.token}")
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            response.json(),
-            {
-                "image": ["This field is required."],
-                "parents": ["This field is required."],
-                "slug": ["This field is required."],
-            },
-        )
-
-    def test_task_create_no_parent(self):
-        response = self.client.post(
-            reverse("api:task-create"),
-            data={
-                "process_id": str(self.process.id),
-                "slug": "test_task",
-                "image": "registry.gitlab.com/test",
-                "parents": [],
-            },
-            HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            format="json",
-        )
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            response.json(), {"parents": ["This list may not be empty."]}
-        )
-
-    def test_task_create_distinct_workflows_on_parents(self):
-        process2 = Process.objects.create(
-            farm=self.wheat_farm,
-            mode=ProcessMode.Repository,
-            creator=self.superuser,
-        )
-        task3 = process2.tasks.create(
-            run=0,
-            depth=1,
-            slug="task_parent",
-            image="registry.gitlab.com/test",
-        )
-
-        response = self.client.post(
-            reverse("api:task-create"),
-            data={
-                "process_id": str(self.process.id),
-                "slug": "test_task",
-                "image": "registry.gitlab.com/test",
-                "parents": [str(self.task1.id), str(self.task2.id), str(task3.id)],
-            },
-            HTTP_AUTHORIZATION=f"Ponos {task3.token}",
-            format="json",
-        )
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            response.json(),
-            {
-                "non_field_errors": [
-                    "All parents must be in the same process as the child task"
-                ]
-            },
-        )
-
-    def test_task_create_distinct_runs_on_parents(self):
-        task3 = self.process.tasks.create(
-            run=1,
-            depth=1,
-            slug="task_parent",
-            image="registry.gitlab.com/test",
-        )
-
-        response = self.client.post(
-            reverse("api:task-create"),
-            data={
-                "process_id": str(self.process.id),
-                "slug": "test_task",
-                "image": "registry.gitlab.com/test",
-                "parents": [str(self.task1.id), str(self.task2.id), str(task3.id)],
-            },
-            HTTP_AUTHORIZATION=f"Ponos {task3.token}",
-            format="json",
-        )
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            response.json(),
-            {
-                "non_field_errors": [
-                    "All parents must have the same run in the given process"
-                ]
-            },
-        )
-
-    def test_task_create_duplicate(self):
-        task = self.process.tasks.create(
-            run=0,
-            depth=3,
-            slug="sibling",
-            image="registry.gitlab.com/test",
-        )
-
-        response = self.client.post(
-            reverse("api:task-create"),
-            data={
-                "process_id": str(self.process.id),
-                "slug": "sibling",
-                "image": "registry.gitlab.com/test",
-                "parents": [str(self.task1.id), str(self.task2.id)],
-            },
-            HTTP_AUTHORIZATION=f"Ponos {task.token}",
-            format="json",
-        )
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            response.json(),
-            {
-                "non_field_errors": [
-                    "A task with the `sibling` slug already exists in run 0."
-                ]
-            },
-        )
-
-    def test_task_create(self):
-        task3 = self.process.tasks.create(
-            run=0,
-            depth=3,
-            slug="task_parent",
-            image="registry.gitlab.com/test",
-        )
-
-        with self.assertNumQueries(9):
-            response = self.client.post(
-                reverse("api:task-create"),
-                data={
-                    "process_id": str(self.process.id),
-                    "slug": "test_task",
-                    "image": "registry.gitlab.com/test",
-                    "parents": [str(self.task1.id), str(self.task2.id), str(task3.id)],
-                    "command": "echo Test",
-                    "env": {"test": "test", "test2": "test2"},
-                },
-                HTTP_AUTHORIZATION=f"Ponos {task3.token}",
-                format="json",
-            )
-            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
-        new_task = self.process.tasks.get(slug="test_task")
-
-        self.assertDictEqual(
-            response.json(),
-            {
-                "id": str(new_task.id),
-                "process_id": str(self.process.id),
-                "slug": "test_task",
-                "parents": [str(self.task1.id), str(self.task2.id), str(task3.id)],
-                "image": "registry.gitlab.com/test",
-                "command": "echo Test",
-                "env": {
-                    "ARKINDEX_API_CSRF_COOKIE": "arkindex.csrf",
-                    "ARKINDEX_API_TOKEN": "deadbeefTestToken",
-                    "ARKINDEX_API_URL": "http://localhost:8000/api/v1/",
-                    "ARKINDEX_CORPUS_ID": str(self.process.corpus.id),
-                    "ARKINDEX_PROCESS_ID": str(self.process.id),
-                    "test": "test",
-                    "test2": "test2",
-                    "ARKINDEX_TASK_TOKEN": new_task.token,
-                },
-                "run": 0,
-                "depth": 4,
-                "has_docker_socket": False,
-            },
-        )
-
-    def test_task_create_has_docker_socket_true(self):
-        task3 = self.process.tasks.create(
-            run=0,
-            depth=3,
-            slug="task_parent",
-            image="registry.gitlab.com/test",
-        )
-
-        with self.assertNumQueries(9):
-            response = self.client.post(
-                reverse("api:task-create"),
-                data={
-                    "process_id": str(self.process.id),
-                    "slug": "test_task",
-                    "image": "registry.gitlab.com/test",
-                    "parents": [str(self.task1.id), str(self.task2.id), str(task3.id)],
-                    "command": "echo Test",
-                    "env": {"test": "test", "test2": "test2"},
-                    "has_docker_socket": True,
-                },
-                HTTP_AUTHORIZATION=f"Ponos {task3.token}",
-                format="json",
-            )
-            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
-        new_task = self.process.tasks.get(slug="test_task")
-
-        self.assertDictEqual(
-            response.json(),
-            {
-                "id": str(new_task.id),
-                "process_id": str(self.process.id),
-                "slug": "test_task",
-                "parents": [str(self.task1.id), str(self.task2.id), str(task3.id)],
-                "image": "registry.gitlab.com/test",
-                "command": "echo Test",
-                "env": {
-                    "ARKINDEX_API_CSRF_COOKIE": "arkindex.csrf",
-                    "ARKINDEX_API_TOKEN": "deadbeefTestToken",
-                    "ARKINDEX_API_URL": "http://localhost:8000/api/v1/",
-                    "ARKINDEX_CORPUS_ID": str(self.process.corpus.id),
-                    "ARKINDEX_PROCESS_ID": str(self.process.id),
-                    "test": "test",
-                    "test2": "test2",
-                    "ARKINDEX_TASK_TOKEN": new_task.token,
-                },
-                "run": 0,
-                "depth": 4,
-                "has_docker_socket": True,
-            },
-        )
-
-    def test_retrieve_secret_requires_auth(self):
-        """
-        Only agents or tasks may access a secret details
-        """
-        with self.assertNumQueries(0):
-            response = self.client.get(
-                reverse("api:secret-details", kwargs={"name": "abc"})
-            )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-        self.assertDictEqual(
-            response.json(), {"detail": "Authentication credentials were not provided."}
-        )
-
-    def test_retrieve_secret_not_found(self):
-        """
-        A 404 should be raised when no secret match query name
-        """
-        with self.assertNumQueries(2):
-            response = self.client.get(
-                reverse(
-                    "api:secret-details", kwargs={"name": "the_most_important_secret"}
-                ),
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-        self.assertDictEqual(response.json(), {"detail": "Not found."})
-
-    @override_settings(PONOS_PRIVATE_KEY=PONOS_PRIVATE_KEY)
-    def test_retrieve_secret_agent_forbidden(self):
-        """
-        Agent should be able to retrieve a secret content as cleartext
-        """
-        account_name = "bank_account/0001/private"
-        secret = Secret.objects.create(
-            name=account_name,
-            nonce=b"1337" * 4,
-            content=encrypt(b"1337" * 4, "1337$"),
-        )
-        self.assertEqual(secret.content, b"\xc1\x81\xc0\xceo")
-
-        with self.assertNumQueries(0):
-            response = self.client.get(
-                reverse("api:secret-details", kwargs={"name": account_name}),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    @override_settings(PONOS_PRIVATE_KEY=PONOS_PRIVATE_KEY)
-    def test_retrieve_secret_forbid_users(self):
-        account_name = "bank_account/0001/private"
-        secret = Secret.objects.create(
-            name=account_name,
-            nonce=b"1337" * 4,
-            content=encrypt(b"1337" * 4, "1337$"),
-        )
-        self.assertEqual(secret.content, b"\xc1\x81\xc0\xceo")
-
-        for user in [self.user, self.superuser]:
-            with self.subTest(user=user):
-                self.client.force_login(user)
-                with self.assertNumQueries(0):
-                    response = self.client.get(reverse("api:secret-details", kwargs={"name": account_name}))
-                    self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    @override_settings(PONOS_PRIVATE_KEY=PONOS_PRIVATE_KEY)
-    def test_task_retrieve_secret_requires_active_user(self):
-        account_name = "bank_account/0001/private"
-        secret = Secret.objects.create(
-            name=account_name,
-            nonce=b"1337" * 4,
-            content=encrypt(b"1337" * 4, "1337$"),
-        )
-        self.assertEqual(secret.content, b"\xc1\x81\xc0\xceo")
-
-        Process.objects.create(
-            creator=self.user,
-            mode=ProcessMode.Repository,
-        )
-        self.user.is_active = False
-        self.user.save()
-
-        with self.assertNumQueries(1):
-            response = self.client.get(
-                reverse("api:secret-details", kwargs={"name": account_name}),
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-        self.assertDictEqual(
-            response.json(),
-            {"detail": "User inactive or deleted."},
-        )
-
-    @override_settings(PONOS_PRIVATE_KEY=PONOS_PRIVATE_KEY)
-    def test_task_retrieve_secret(self):
-        account_name = "bank_account/0001/private"
-        secret = Secret.objects.create(
-            name=account_name,
-            nonce=b"1337" * 4,
-            content=encrypt(b"1337" * 4, "1337$"),
-        )
-        self.assertEqual(secret.content, b"\xc1\x81\xc0\xceo")
-
-        corpus = Corpus.objects.create(name="Test corpus")
-        corpus.processes.create(
-            mode=ProcessMode.Workers,
-            creator=self.user,
-        )
-
-        with self.assertNumQueries(2):
-            response = self.client.get(
-                reverse("api:secret-details", kwargs={"name": account_name}),
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.assertDictEqual(
-            response.json(),
-            {
-                "id": str(secret.id),
-                "name": account_name,
-                "content": "1337$",
-            },
-        )
-
-    def test_list_agents_requires_login(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(reverse("api:agents-state"))
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_list_agents_agent_forbidden(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(
-                reverse("api:agents-state"),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_list_agents_task_forbidden(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(
-                reverse("api:agents-state"),
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_list_agents_requires_verified(self):
-        self.user.verified_email = False
-        self.user.save()
-        self.client.force_login(self.user)
-        with self.assertNumQueries(2):
-            response = self.client.get(reverse("api:agents-state"))
-            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_list_agents(self):
-        """
-        Lists agents from all farms with their status
-        """
-        # Add tasks with different states on the agent
-        self.agent.tasks.bulk_create(
-            [
-                Task(
-                    run=0,
-                    depth=0,
-                    process=self.process,
-                    slug=state.value,
-                    state=state,
-                    agent=self.agent,
-                )
-                for state in State
-            ]
-        )
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(6):
-            response = self.client.get(reverse("api:agents-state"))
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        data = response.json()
-        self.assertEqual(data["count"], 1)
-        agent_state = data["results"][0]
-        del agent_state["last_ping"]
-        self.assertDictEqual(
-            agent_state,
-            {
-                "id": str(self.agent.id),
-                "active": True,
-                "cpu_cores": 2,
-                "cpu_frequency": 1000000000,
-                "cpu_load": .1,
-                "farm": {
-                    "id": str(self.wheat_farm.id),
-                    "name": "Wheat farm",
-                },
-                "gpus": [
-                    {
-                        "id": "108c6524-c63a-4811-bbed-9723d32a0688",
-                        "index": 0,
-                        "name": "GPU1",
-                        "ram_total": 2147483648,
-                    },
-                    {
-                        "id": "f30d1407-92bb-484b-84b0-0b8bae41ca91",
-                        "index": 1,
-                        "name": "GPU2",
-                        "ram_total": 8589934592,
-                    },
-                ],
-                "hostname": "ghostname",
-                "ram_load": 100000000,
-                "ram_total": 2000000000,
-                "running_tasks_count": 1,
-            },
-        )
-
-    def test_retrieve_agent_requires_login(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(
-                reverse("api:agent-details", kwargs={"pk": str(self.agent.id)}),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_retrieve_agent_requires_verified(self):
-        self.user.verified_email = False
-        self.user.save()
-        self.client.force_login(self.user)
-
-        with self.assertNumQueries(2):
-            response = self.client.get(
-                reverse("api:agent-details", kwargs={"pk": str(self.agent.id)}),
-            )
-            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_retrieve_agent_details(self):
-        """
-        The view returns an agents with its details and associated running tasks
-        """
-        # Add tasks with different states on the agent
-        Task.objects.bulk_create(
-            [
-                Task(
-                    run=0,
-                    depth=0,
-                    process=self.process,
-                    slug=state.value,
-                    state=state,
-                    agent=self.agent,
-                )
-                for state in State
-            ]
-        )
-        running_task = self.agent.tasks.get(state=State.Running)
-        self.client.force_login(self.user)
-
-        with self.assertNumQueries(6):
-            response = self.client.get(
-                reverse("api:agent-details", kwargs={"pk": str(self.agent.id)})
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        data = response.json()
-        del data["last_ping"]
-        self.assertDictEqual(
-            data,
-            {
-                "id": str(self.agent.id),
-                "active": True,
-                "cpu_cores": 2,
-                "cpu_frequency": 1000000000,
-                "cpu_load": .1,
-                "farm": {
-                    "id": str(self.wheat_farm.id),
-                    "name": "Wheat farm",
-                },
-                "gpus": [
-                    {
-                        "id": "108c6524-c63a-4811-bbed-9723d32a0688",
-                        "index": 0,
-                        "name": "GPU1",
-                        "ram_total": 2147483648,
-                    },
-                    {
-                        "id": "f30d1407-92bb-484b-84b0-0b8bae41ca91",
-                        "index": 1,
-                        "name": "GPU2",
-                        "ram_total": 8589934592,
-                    },
-                ],
-                "hostname": "ghostname",
-                "ram_load": 100000000,
-                "ram_total": 2000000000,
-                "running_tasks": [
-                    {
-                        "id": str(running_task.id),
-                        "run": 0,
-                        "depth": 0,
-                        "parents": [],
-                        "slug": "running",
-                        "state": "running",
-                        "shm_size": None,
-                    }
-                ],
-            },
-        )
-
-    def test_retrieve_agent_agent_forbidden(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(
-                reverse("api:agent-details", kwargs={"pk": str(self.agent.id)}),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_retrieve_agent_task_forbidden(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(
-                reverse("api:agent-details", kwargs={"pk": str(self.agent.id)}),
-                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_list_farms_requires_login(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(reverse("api:farm-list"))
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_list_farms_requires_verified(self):
-        self.user.verified_email = False
-        self.user.save()
-        self.client.force_login(self.user)
-        with self.assertNumQueries(2):
-            response = self.client.get(reverse("api:farm-list"))
-            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_list_farms_agent_forbidden(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(reverse("api:farm-list"), HTTP_AUTHORIZATION=f"Bearer {self.agent.token}")
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_list_farms_task_forbidden(self):
-        with self.assertNumQueries(0):
-            response = self.client.get(reverse("api:farm-list"), HTTP_AUTHORIZATION=f"Ponos {self.task1.token}")
-            self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
-    def test_list_farms(self):
-        self.client.force_login(self.superuser)
-
-        with self.assertNumQueries(4):
-            response = self.client.get(reverse("api:farm-list"))
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.assertDictEqual(
-            response.json(),
-            {
-                "count": 2,
-                "number": 1,
-                "previous": None,
-                "next": None,
-                "results": [
-                    {"id": str(self.default_farm.id), "name": "Default farm"},
-                    {"id": str(self.wheat_farm.id), "name": "Wheat farm"},
-                ],
-            },
-        )
-
-    def test_list_farms_guest(self):
-        self.client.force_login(self.user)
-        self.assertFalse(self.default_farm.is_available(self.user))
-        self.assertTrue(self.wheat_farm.is_available(self.user))
-        # Accessing memberships causes content types to be cached,
-        # so clear again to get the amount of queries for the worst case
-        self.clear_caches()
-
-        with self.assertNumQueries(6):
-            response = self.client.get(reverse("api:farm-list"))
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.assertDictEqual(
-            response.json(),
-            {
-                "count": 1,
-                "number": 1,
-                "previous": None,
-                "next": None,
-                "results": [
-                    {"id": str(self.wheat_farm.id), "name": "Wheat farm"},
-                ],
-            },
-        )
-
-    @override_settings(PONOS_PRIVATE_KEY=PONOS_PRIVATE_KEY)
-    def test_public_key(self):
-        expected = load_pem_private_key(
-            PONOS_PRIVATE_KEY.read_bytes(),
-            password=None,
-            backend=default_backend(),
-        ).public_key().public_bytes(
-            Encoding.PEM,
-            PublicFormat.SubjectPublicKeyInfo,
-        )
-
-        with self.assertNumQueries(0):
-            response = self.client.get(reverse("api:public-key"))
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.assertEqual(response.content, expected)
-
-    def test_agent_refresh(self):
-        with self.assertNumQueries(0):
-            response = self.client.post(
-                reverse("api:agent-token-refresh"),
-                {"refresh": str(self.agent.token)},
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        data = response.json()
-        self.assertCountEqual(list(data.keys()), ["access", "refresh"])
-        access_token = AccessToken(data["access"])
-        refresh_token = RefreshToken(data["refresh"])
-        access_token.verify()
-        refresh_token.verify()
-        self.assertEqual(access_token.payload["agent_id"], str(self.agent.id))
-        self.assertEqual(refresh_token.payload["agent_id"], str(self.agent.id))
+                resp = self.client.get(reverse("api:task-details", args=[self.task1.id]))
+                self.assertEqual(resp.status_code, status.HTTP_200_OK)
+                self.assertEqual(resp.json()["logs"], "")
diff --git a/arkindex/ponos/tests/test_artifacts_api.py b/arkindex/ponos/tests/test_artifacts_api.py
index 76cf4e6659..5e3266f7ed 100644
--- a/arkindex/ponos/tests/test_artifacts_api.py
+++ b/arkindex/ponos/tests/test_artifacts_api.py
@@ -1,18 +1,13 @@
-import hashlib
-import uuid
+from unittest import expectedFailure
 
 from django.conf import settings
 from django.test import override_settings
 from django.urls import reverse
-from django.utils import timezone
 from rest_framework import status
 
 from arkindex.documents.models import Corpus
-from arkindex.ponos.authentication import AgentUser
-from arkindex.ponos.models import Farm
 from arkindex.process.models import Process, ProcessMode, Repository
 from arkindex.project.tests import FixtureAPITestCase
-from arkindex.project.tools import build_public_key
 from arkindex.users.models import Right, Role, User
 
 
@@ -22,18 +17,6 @@ class TestAPI(FixtureAPITestCase):
     @classmethod
     def setUpTestData(cls):
         super().setUpTestData()
-        cls.wheat_farm = Farm.objects.get(name="Wheat farm")
-        pubkey = build_public_key()
-        cls.agent = AgentUser.objects.create(
-            id=uuid.UUID(hashlib.md5(pubkey.encode("utf-8")).hexdigest()),
-            farm=cls.wheat_farm,
-            hostname="ghostname",
-            cpu_cores=2,
-            cpu_frequency=1e9,
-            public_key=pubkey,
-            ram_total=2e9,
-            last_ping=timezone.now(),
-        )
 
         cls.repo = Repository.objects.first()
         cls.repository_process = Process.objects.create(
@@ -57,7 +40,6 @@ class TestAPI(FixtureAPITestCase):
             mode=ProcessMode.Workers,
             creator=new_user,
             corpus=new_corpus,
-            farm=cls.wheat_farm,
         )
         cls.process2.run()
         cls.task3 = cls.process2.tasks.first()
@@ -87,6 +69,7 @@ class TestAPI(FixtureAPITestCase):
             response = self.client.get(reverse("api:task-artifacts", args=[self.task1.id]))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_list_requires_process_guest(self):
         self.process.creator = self.superuser
         self.process.save()
@@ -108,7 +91,7 @@ class TestAPI(FixtureAPITestCase):
             with self.subTest(role=role):
                 self.corpus.memberships.filter(user=self.user).update(level=role.value)
 
-                with self.assertNumQueries(6):
+                with self.assertNumQueries(4):
                     response = self.client.get(reverse("api:task-artifacts", args=[self.task1.id]))
                     self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -145,7 +128,7 @@ class TestAPI(FixtureAPITestCase):
                 membership.level = role.value
                 membership.save()
 
-                with self.assertNumQueries(7):
+                with self.assertNumQueries(4):
                     response = self.client.get(reverse("api:task-artifacts", args=[self.task1.id]))
                     self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -203,38 +186,6 @@ class TestAPI(FixtureAPITestCase):
             ],
         )
 
-    def test_list_agent(self):
-        with self.assertNumQueries(3):
-            response = self.client.get(
-                reverse("api:task-artifacts", args=[self.task1.id]),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.assertListEqual(
-            response.json(),
-            [
-                {
-                    "content_type": "application/json",
-                    "created": self.artifact1.created.isoformat().replace("+00:00", "Z"),
-                    "id": str(self.artifact1.id),
-                    "path": "path/to/file.json",
-                    "s3_put_url": None,
-                    "size": 42,
-                    "updated": self.artifact1.updated.isoformat().replace("+00:00", "Z"),
-                },
-                {
-                    "content_type": "text/plain",
-                    "created": self.artifact2.created.isoformat().replace("+00:00", "Z"),
-                    "id": str(self.artifact2.id),
-                    "path": "some/text.txt",
-                    "s3_put_url": None,
-                    "size": 1337,
-                    "updated": self.artifact2.updated.isoformat().replace("+00:00", "Z"),
-                },
-            ],
-        )
-
     def test_list_other_task_no_access(self):
         with self.assertNumQueries(2):
             response = self.client.get(
@@ -244,7 +195,7 @@ class TestAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
     def test_list_other_task_with_access(self):
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(
                 reverse("api:task-artifacts", args=[self.task1.id]),
                 HTTP_AUTHORIZATION=f"Ponos {self.task2.token}",
@@ -311,60 +262,8 @@ class TestAPI(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    @override_settings(PONOS_ARTIFACT_MAX_SIZE=99999)
-    def test_create_unassigned_agent(self):
-        self.assertNotEqual(self.task1.agent, self.agent)
-
-        with self.assertNumQueries(2):
-            response = self.client.post(
-                reverse("api:task-artifacts", args=[self.task1.id]),
-                data={"path": "some/path.txt", "content_type": "text/plain", "size": 1000},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    @override_settings(PONOS_ARTIFACT_MAX_SIZE=99999)
-    def test_create_assigned_agent(self):
-        self.task1.agent = self.agent
-        self.task1.save()
-
-        with self.assertNumQueries(5):
-            response = self.client.post(
-                reverse("api:task-artifacts", args=[self.task1.id]),
-                data={"path": "some/path.txt", "content_type": "text/plain", "size": 1000},
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
-        # Check response has a valid S3 put URL, without matching the parameters in the querystring
-        data = response.json()
-        s3_put_url = data.get("s3_put_url")
-        self.assertIsNotNone(s3_put_url)
-        del data["s3_put_url"]
-        self.assertTrue(
-            s3_put_url.startswith(
-                f"http://s3/ponos-artifacts/{self.task1.id}/some/path.txt"
-            )
-        )
-
-        # An artifact has been created
-        artifact = self.task1.artifacts.get(path="some/path.txt")
-
-        self.assertDictEqual(
-            response.json(),
-            {
-                "content_type": "text/plain",
-                "created": artifact.created.isoformat().replace("+00:00", "Z"),
-                "id": str(artifact.id),
-                "path": "some/path.txt",
-                "size": 1000,
-                "updated": artifact.updated.isoformat().replace("+00:00", "Z"),
-            },
-        )
-
     @override_settings(PONOS_ARTIFACT_MAX_SIZE=99999)
     def test_create_unique_path(self):
-        self.task1.agent = self.agent
         self.task1.save()
         self.task1.artifacts.create(
             path="some/path.txt",
@@ -380,9 +279,10 @@ class TestAPI(FixtureAPITestCase):
                     "content_type": "text/plain",
                     "size": 10243,
                 },
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+                HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
         self.assertDictEqual(
             response.json(), {"path": ["An artifact with this path already exists"]}
@@ -390,7 +290,6 @@ class TestAPI(FixtureAPITestCase):
 
     @override_settings()
     def test_create_size_limits(self):
-        self.task1.agent = self.agent
         self.task1.save()
 
         params = [
@@ -420,13 +319,13 @@ class TestAPI(FixtureAPITestCase):
                         "content_type": "text/plain",
                         "size": size,
                     },
-                    HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
+                    HTTP_AUTHORIZATION=f"Ponos {self.task1.token}",
                 )
                 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
                 self.assertEqual(response.json(), {"size": [expected_error]})
 
     def test_create_other_task(self):
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.post(
                 reverse("api:task-artifacts", args=[self.task1.id]),
                 data={
@@ -491,6 +390,7 @@ class TestAPI(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_download_requires_process_guest(self):
         self.corpus.memberships.filter(user=self.user).delete()
         self.corpus.public = False
@@ -510,7 +410,7 @@ class TestAPI(FixtureAPITestCase):
             with self.subTest(role=role):
                 self.corpus.memberships.filter(user=self.user).update(level=role.value)
 
-                with self.assertNumQueries(5):
+                with self.assertNumQueries(3):
                     response = self.client.get(
                         reverse("api:task-artifact-download", args=[self.task1.id, "path/to/file.json"]),
                     )
@@ -535,7 +435,7 @@ class TestAPI(FixtureAPITestCase):
                 membership.level = role.value
                 membership.save()
 
-                with self.assertNumQueries(6):
+                with self.assertNumQueries(3):
                     response = self.client.get(
                         reverse("api:task-artifact-download", args=[task.id, "path/to/file.json"]),
                     )
@@ -548,32 +448,6 @@ class TestAPI(FixtureAPITestCase):
                     )
                 )
 
-    def test_download_not_found(self):
-        with self.assertNumQueries(2):
-            response = self.client.get(
-                reverse("api:task-artifact-download", args=[self.task1.id, "nope.xxx"]),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
-    def test_download_agent(self):
-        # Being assigned to the task is not required
-        self.assertNotEqual(self.task1.agent, self.agent)
-
-        with self.assertNumQueries(2):
-            response = self.client.get(
-                reverse("api:task-artifact-download", args=[self.task1.id, "path/to/file.json"]),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-            )
-            self.assertEqual(response.status_code, status.HTTP_302_FOUND)
-
-        self.assertTrue(response.has_header("Location"))
-        self.assertTrue(
-            response["Location"].startswith(
-                f"http://s3/ponos-artifacts/{self.task1.id}/path/to/file.json"
-            )
-        )
-
     def test_download_task(self):
         with self.assertNumQueries(2):
             response = self.client.get(
@@ -590,7 +464,7 @@ class TestAPI(FixtureAPITestCase):
         )
 
     def test_download_other_task_with_access(self):
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.get(
                 reverse("api:task-artifact-download", args=[self.task1.id, "path/to/file.json"]),
                 HTTP_AUTHORIZATION=f"Ponos {self.task2.token}",
diff --git a/arkindex/ponos/tests/test_keys.py b/arkindex/ponos/tests/test_keys.py
deleted file mode 100644
index f1d397e970..0000000000
--- a/arkindex/ponos/tests/test_keys.py
+++ /dev/null
@@ -1,89 +0,0 @@
-import os
-import tempfile
-
-import cryptography
-from django.test import TestCase, override_settings
-
-from arkindex.ponos.keys import gen_private_key, load_private_key
-
-BAD_KEY = """-----BEGIN RSA PRIVATE KEY-----
-MCoCAQACBGvoyx0CAwEAAQIEUg2X0QIDAMUbAgMAjCcCAmr9AgJMawICKvo=
------END RSA PRIVATE KEY-----"""
-
-
-@override_settings(PONOS_PRIVATE_KEY=None)
-class KeysTestCase(TestCase):
-    """
-    Test ECDH keys
-    """
-
-    def test_no_key(self):
-        """
-        No key by default with those settings
-        """
-        with self.settings(DEBUG=True):
-            # On debug, that works, but with a warning
-            self.assertIsNone(load_private_key())
-
-        with self.settings(DEBUG=False):
-            # On prod, that fails
-            with self.assertRaisesRegex(
-                Exception, r"Missing setting PONOS_PRIVATE_KEY"
-            ):
-                load_private_key()
-
-        with self.settings(DEBUG=True, PONOS_PRIVATE_KEY="/tmp/nope"):
-            # On debug, that works, but with a warning
-            self.assertIsNone(load_private_key())
-
-        with self.settings(DEBUG=False, PONOS_PRIVATE_KEY="/tmp/nope"):
-            # On prod, that fails
-            with self.assertRaisesRegex(Exception, r"Invalid PONOS_PRIVATE_KEY path"):
-                load_private_key()
-
-        # Test with a valid RSA key (not ECDH)
-        _, path = tempfile.mkstemp()
-        open(path, "w").write(BAD_KEY)
-        with self.settings(DEBUG=True, PONOS_PRIVATE_KEY=path):
-            # On debug, that fails too !
-            with self.assertRaisesRegex(Exception, r"not an ECDH"):
-                load_private_key()
-
-        with self.settings(DEBUG=False, PONOS_PRIVATE_KEY=path):
-            # On prod, that fails
-            with self.assertRaisesRegex(Exception, r"not an ECDH"):
-                load_private_key()
-        os.unlink(path)
-
-    def test_private_key(self):
-        """
-        Test private key writing and loading
-        """
-
-        # Generate some key
-        _, path = tempfile.mkstemp()
-        gen_private_key(path)
-        key = open(path).read().splitlines()
-        self.assertTrue(len(key) > 2)
-        self.assertEqual(key[0], "-----BEGIN PRIVATE KEY-----")
-        self.assertEqual(key[-1], "-----END PRIVATE KEY-----")
-
-        # Load it through settings
-        with self.settings(PONOS_PRIVATE_KEY=path):
-            priv = load_private_key()
-            self.assertTrue(
-                isinstance(
-                    priv,
-                    cryptography.hazmat.backends.openssl.ec._EllipticCurvePrivateKey,
-                )
-            )
-            self.assertTrue(priv.key_size > 256)
-
-        # When messed up, nothing works
-        with open(path, "w") as f:
-            f.seek(100)
-            f.write("coffee")
-        with self.settings(PONOS_PRIVATE_KEY=path):
-            with self.assertRaisesRegex(Exception, r"Could not deserialize key data"):
-                load_private_key()
-        os.unlink(path)
diff --git a/arkindex/ponos/tests/test_models.py b/arkindex/ponos/tests/test_models.py
index ed04ea1071..f8e884911f 100644
--- a/arkindex/ponos/tests/test_models.py
+++ b/arkindex/ponos/tests/test_models.py
@@ -1,12 +1,9 @@
-import tempfile
 from unittest.mock import patch
 
-from django.core.exceptions import ValidationError
 from django.db.models import prefetch_related_objects
-from django.test import override_settings
 from django.utils import timezone
 
-from arkindex.ponos.models import FINAL_STATES, Agent, Farm, Secret, State, build_aes_cipher, encrypt
+from arkindex.ponos.models import FINAL_STATES, State
 from arkindex.process.models import ProcessMode
 from arkindex.project.tests import FixtureAPITestCase
 
@@ -15,39 +12,14 @@ class TestModels(FixtureAPITestCase):
     @classmethod
     def setUpTestData(cls):
         super().setUpTestData()
-        cls.farm = Farm.objects.create(name="Cryptominers")
         cls.process = cls.corpus.processes.create(
             creator=cls.user,
             mode=ProcessMode.Workers,
-            farm=cls.farm,
         )
         cls.process.run()
         cls.task1 = cls.process.tasks.first()
         cls.nonce = b"42" + b"0" * 14
 
-    def setUp(self):
-        self.agent = Agent.objects.create(
-            farm=self.farm,
-            hostname="roastname",
-            cpu_cores=100,
-            cpu_frequency=1e9,
-            public_key="Mamoswine",
-            ram_total=10e9,
-            last_ping=timezone.now(),
-        )
-        self.agent.gpus.create(
-            id="108c6524-c63a-4811-bbed-9723d32a0688",
-            name="GPU1",
-            index=0,
-            ram_total=2 * 1024 * 1024 * 1024,
-        )
-        self.agent.gpus.create(
-            id="f30d1407-92bb-484b-84b0-0b8bae41ca91",
-            name="GPU2",
-            index=1,
-            ram_total=8 * 1024 * 1024 * 1024,
-        )
-
     def test_is_final(self):
         for state in State:
             self.task1.state = state
@@ -67,72 +39,6 @@ class TestModels(FixtureAPITestCase):
                     self.process.is_final, msg="{} should not be final".format(state)
                 )
 
-    def test_delete_agent_non_final(self):
-        """
-        Agent deletion should be prevented when it has non-final tasks
-        """
-        self.task1.agent = self.agent
-        self.task1.state = State.Pending
-        self.task1.save()
-        with self.assertRaisesRegex(ValidationError, "non-final"):
-            self.agent.delete()
-
-        self.task1.state = State.Error
-        self.task1.save()
-        # Should no longer fail
-        self.agent.delete()
-
-        self.task1.refresh_from_db()
-        self.assertIsNone(self.task1.agent)
-
-    @override_settings(PONOS_PRIVATE_KEY=None)
-    def test_aes_missing_key(self):
-        with self.assertRaisesRegex(Exception, r"Missing a PONOS_PRIVATE_KEY"):
-            build_aes_cipher(nonce=self.nonce)
-
-    @patch("arkindex.ponos.models.settings")
-    def test_build_aes_cipher(self, settings_mock):
-        """
-        AES encryption key should be a derivate of the Ponos server secret key
-        """
-        _, path = tempfile.mkstemp()
-        settings_mock.PONOS_PRIVATE_KEY = path
-        with open(path, "wb") as f:
-            f.write(b"pikachu")
-        cipher = build_aes_cipher(nonce=self.nonce)
-        self.assertEqual(cipher.encryptor().update(b"hey"), b"lM\x8d")
-        with open(path, "wb") as f:
-            f.write(b"bulbasaur")
-        cipher = build_aes_cipher(nonce=self.nonce)
-        self.assertNotEqual(cipher.encryptor().update(b"hey"), b"lM\x8d")
-
-    @patch("arkindex.ponos.models.settings")
-    def test_secret_encrypt_decrypt(self, settings_mock):
-        _, path = tempfile.mkstemp()
-        settings_mock.PONOS_PRIVATE_KEY = path
-        with open(path, "wb") as f:
-            f.write(b"pikachu")
-        secret = Secret(
-            name="Test secret",
-            nonce=self.nonce,
-            content=encrypt(self.nonce, "secret_m3ssage"),
-        )
-        self.assertEqual(secret.content, b"wM\x97\n\xadS\x13\x8a\x89&ZF\xbd\xee")
-        self.assertEqual(secret.decrypt(), "secret_m3ssage")
-
-    def test_agent_estimate_new_tasks_cost(self):
-        """
-        Agent has 100 cores and 10GB of RAM
-        One task is estimated to use 1 CPU (1%) and 1GB of RAM (10%)
-        """
-        self.agent.cpu_load = 50
-        self.agent.ram_load = 0.35
-        self.agent.save()
-        # The CPU will define the agent load for the first task reaching 51% occupancy
-        self.assertEqual(self.agent._estimate_new_tasks_cost(tasks=1), 0.51)
-        # For the second task, the RAM will reach 55% occupancy overtaking the CPU load (52%)
-        self.assertEqual(self.agent._estimate_new_tasks_cost(tasks=2), 0.55)
-
     def test_requires_gpu(self):
         """
         Check the GPU generated requirements rules
diff --git a/arkindex/ponos/tests/test_rq_tasks.py b/arkindex/ponos/tests/test_rq_tasks.py
new file mode 100644
index 0000000000..f029afb91f
--- /dev/null
+++ b/arkindex/ponos/tests/test_rq_tasks.py
@@ -0,0 +1,53 @@
+from unittest.mock import call, patch
+
+from django.test import override_settings
+
+from arkindex.ponos.models import Farm
+from arkindex.process.models import ProcessMode, WorkerVersion
+from arkindex.project.tests import FixtureAPITestCase
+
+
+class TestModels(FixtureAPITestCase):
+
+    @classmethod
+    def setUpTestData(cls):
+        super().setUpTestData()
+        cls.farm = Farm.objects.create(name="Farm")
+        cls.process = cls.corpus.processes.create(
+            creator=cls.user,
+            mode=ProcessMode.Workers,
+            corpus=cls.corpus,
+            farm=cls.farm,
+        )
+        cls.worker_version1 = WorkerVersion.objects.get(worker__slug="reco")
+        cls.worker_version2 = WorkerVersion.objects.get(worker__slug="dla")
+        cls.run1 = cls.process.worker_runs.create(version=cls.worker_version1, parents=[])
+        cls.run2 = cls.process.worker_runs.create(version=cls.worker_version2, parents=[cls.run1.id])
+
+    @override_settings(PONOS_RQ_EXECUTION=True)
+    @patch("arkindex.ponos.tasks.run_task_rq.delay")
+    def test_run_process_schedules_tasks(self, run_task_mock):
+        run_task_mock.side_effect = ["init_job", "job1", "job2"]
+        # on_commit will not be called automatically as savepoints are used for tests transactions
+        with self.captureOnCommitCallbacks(execute=True):
+            self.process.run()
+        init, t1, t2 = self.process.tasks.order_by("depth")
+        self.assertEqual(run_task_mock.call_count, 3)
+        call1, call2, call3 = run_task_mock.call_args_list
+        self.assertEqual(call1, call(init))
+        self.assertTupleEqual(call2.args, (t1,))
+        self.assertListEqual(list(call2.kwargs.keys()), ["depends_on"])
+        task1_depends = call2.kwargs["depends_on"]
+        self.assertEqual(vars(task1_depends), {
+            "dependencies": ["init_job"],
+            "allow_failure": True,
+            "enqueue_at_front": False,
+        })
+        self.assertTupleEqual(call3.args, (t2,))
+        self.assertListEqual(list(call3.kwargs.keys()), ["depends_on"])
+        task2_depends = call3.kwargs["depends_on"]
+        self.assertEqual(vars(task2_depends), {
+            "dependencies": ["job1"],
+            "allow_failure": True,
+            "enqueue_at_front": False,
+        })
diff --git a/arkindex/ponos/tests/test_tasks_attribution.py b/arkindex/ponos/tests/test_tasks_attribution.py
deleted file mode 100644
index 1d09619a82..0000000000
--- a/arkindex/ponos/tests/test_tasks_attribution.py
+++ /dev/null
@@ -1,481 +0,0 @@
-import uuid
-from datetime import timedelta
-from unittest.mock import patch
-
-from django.utils import timezone
-
-from arkindex.ponos.models import ACTIVE_STATES, FINAL_STATES, Agent, Farm, State, Task
-from arkindex.ponos.models import timezone as model_tz
-from arkindex.process.models import Process, ProcessMode, WorkerVersion, WorkerVersionState
-from arkindex.project.tests import FixtureTestCase
-
-
-class TasksAttributionTestCase(FixtureTestCase):
-    """
-    Ponos server distribute tasks equally among agents.
-    """
-
-    @classmethod
-    def setUpTestData(cls):
-        super().setUpTestData()
-        cls.farm = Farm.objects.get(name="Wheat farm")
-        cls.process = Process.objects.create(creator=cls.superuser, mode=ProcessMode.Repository, farm=cls.farm)
-        # Remove all task fixtures so we can fully control task assignation
-        WorkerVersion.objects.filter(state=WorkerVersionState.Available).update(state=WorkerVersionState.Error)
-        Task.objects.all().delete()
-
-    def _run_tasks(self, tasks):
-        """
-        Mark a list of tasks as running
-        """
-        for t in tasks:
-            t.state = State.Running
-            t.save()
-
-    def _build_agent(self, **kwargs):
-        """
-        Creates an agent
-        Default values may be overridden if passed as kwargs
-        """
-        params = {
-            "hostname": "test_host",
-            "cpu_cores": 2,
-            "cpu_frequency": 4.2e9,
-            "public_key": "",
-            "farm": self.farm,
-            "ram_total": 2e9,
-            "last_ping": timezone.now(),
-            "ram_load": 0.49,
-            "cpu_load": 0.99,
-        }
-        params.update(kwargs)
-        return Agent.objects.create(**params)
-
-    def _add_pending_tasks(self, number, slug_ext="", **kwargs):
-        """
-        Creates pending tasks on the system
-        """
-        params = {
-            "run": 0,
-            "depth": 0,
-            "process": self.process,
-            "state": State.Pending,
-        }
-        params.update(kwargs)
-        return Task.objects.bulk_create(
-            Task(**params, slug=f"slug{slug_ext}_{i}") for i in range(1, number + 1)
-        )
-
-    def _active_agent(self, agent):
-        from arkindex.ponos.models import AGENT_TIMEOUT
-
-        return timezone.now() - agent.last_ping < AGENT_TIMEOUT
-
-    def test_distribute_tasks(self):
-        """
-        Multiple tasks are attributed to 3 registered agents with the same capacity
-        """
-        agent_1, agent_2, agent_3 = [
-            # Build three agents with 10 free CPU cores and 10Go of available RAM
-            self._build_agent(
-                hostname=f"agent_{i}",
-                cpu_cores=11,
-                ram_total=20e9,
-            )
-            for i in range(1, 4)
-        ]
-        self.assertEqual(
-            [a.hostname for a in Agent.objects.all() if self._active_agent(a) is True],
-            ["agent_1", "agent_2", "agent_3"],
-        )
-        # Add 9 pending tasks to the system
-        self._add_pending_tasks(9)
-
-        # As agents are registered, they should retrieve 3 tasks each
-        tasks = agent_1.next_tasks()
-        self.assertEqual(len(tasks), 3)
-        self._run_tasks(tasks)
-
-        # Agent 1 claims tasks before agent 2 but has a too high load to retrieve any task
-        agent_1.ram_load = 0.75
-        agent_1.cpu_load = 3.99
-        agent_1.save()
-        tasks = agent_1.next_tasks()
-        self.assertEqual(len(tasks), 0)
-        bg_tasks = agent_2.next_tasks()
-        self.assertEqual(len(bg_tasks), 3)
-
-        # Agent 1 claims tasks before agent 3
-        tasks = agent_1.next_tasks()
-        self.assertEqual(len(tasks), 0)
-        tasks = agent_3.next_tasks()
-        self.assertEqual(len(tasks), 3)
-
-        # No agent updated its state before agent_2 retrieved its tasks
-        self.assertEqual(len(set(t.id for t in [*bg_tasks, *tasks])), 6)
-
-        self._run_tasks(bg_tasks)
-        self._run_tasks(tasks)
-        self.assertEqual(Task.objects.filter(state=State.Pending).count(), 0)
-
-    def test_distribute_tasks_asymetric(self):
-        """
-        Use case when two agents with different capacity are attributed
-        tasks equivalent to a third of the system capacity
-        """
-        # Agent 1 has 10 free "slots"
-        agent_1 = self._build_agent(hostname="agent_1", cpu_cores=11, ram_total=20e9)
-        # Agent 2 has only 3 free "slots"
-        agent_2 = self._build_agent(hostname="agent_2", cpu_cores=4, ram_total=6e9)
-        # Add 5 pending tasks to the system
-        self._add_pending_tasks(5)
-
-        # The best strategy is to attribute 4 tasks to the first agent and 1 to the second
-        tasks = agent_1.next_tasks()
-        self.assertEqual(len(tasks), 4)
-        self._run_tasks(tasks)
-
-        agent_1.ram_load = 0.7
-        agent_1.cpu_load = 3.99
-        agent_1.save()
-        tasks = agent_1.next_tasks()
-        self.assertEqual(len(tasks), 0)
-        tasks = agent_2.next_tasks()
-        self.assertEqual(len(tasks), 1)
-        self._run_tasks(tasks)
-        self.assertEqual(Task.objects.filter(state=State.Pending).count(), 0)
-
-    def test_distribute_tasks_determinist(self):
-        """
-        Tasks should be distributed in a determinist way depending
-        on the overall load and pending tasks queue
-        Tests that an agent polling task twice retrieves the same tasks
-        """
-        _, _, agent = [
-            self._build_agent(
-                hostname=f"agent_{i}",
-                cpu_cores=11,
-                ram_total=20e9,
-            )
-            for i in range(1, 4)
-        ]
-        self._add_pending_tasks(9)
-        self.assertEqual(
-            [a.hostname for a in Agent.objects.all() if self._active_agent(a) is True],
-            ["agent_1", "agent_2", "agent_3"],
-        )
-
-        tasks = agent.next_tasks()
-        self.assertEqual(len(tasks), 3)
-
-        new_tasks = agent.next_tasks()
-        self.assertCountEqual(tasks, new_tasks)
-
-    @patch("arkindex.ponos.models.AGENT_TIMEOUT", timedelta(seconds=1))
-    def test_non_active_agent(self):
-        """
-        If an agent does not respond, task should be attributed to other running agents
-        """
-        agent_1, agent_2, agent_3 = [
-            self._build_agent(
-                hostname=f"agent_{i}",
-                cpu_cores=11,
-                ram_total=20e9,
-                ram_load=0.1,
-            )
-            for i in range(1, 4)
-        ]
-        self.assertEqual(
-            [a.hostname for a in Agent.objects.all() if self._active_agent(a) is True],
-            ["agent_1", "agent_2", "agent_3"],
-        )
-        # Add 9 pending tasks to the system
-        self._add_pending_tasks(9)
-
-        # As agents are registered, they should retrieve 3 tasks each
-        tasks_agent_1 = agent_1.next_tasks()
-        self.assertEqual(len(tasks_agent_1), 3)
-
-        tasks = agent_2.next_tasks()
-        self.assertEqual(len(tasks), 3)
-
-        self._run_tasks([*tasks_agent_1, *tasks])
-
-        # Jump in the future. Agent 3 did never replied
-        future_now = timezone.now() + timedelta(seconds=2)
-        agent_1.cpu_load = 3.98
-        agent_1.last_ping = future_now
-        agent_1.save()
-        agent_2.cpu_load = 3.99
-        agent_2.last_ping = future_now
-        agent_2.save()
-
-        with patch.object(model_tz, "now") as now_mock:
-            now_mock.return_value = future_now
-            self.assertEqual(
-                [
-                    a.hostname
-                    for a in Agent.objects.all()
-                    if self._active_agent(a) is True
-                ],
-                ["agent_1", "agent_2"],
-            )
-
-            # The 3 remaining tasks should be distributed among 2 agents with similar load
-            tasks_agent_1 = agent_1.next_tasks()
-            self.assertEqual(len(tasks_agent_1), 2)
-
-            tasks = agent_2.next_tasks()
-            self.assertEqual(len(tasks), 1)
-
-            self._run_tasks([*tasks_agent_1, *tasks])
-
-        self.assertEqual(Task.objects.filter(state=State.Pending).count(), 0)
-
-    def test_filter_active_agents(self):
-        """
-        Assert that the DB cost to attribute tasks does
-        not increase with tasks or agents number
-        """
-        agent = self._build_agent()
-        tasks = self._add_pending_tasks(20)
-
-        with self.assertNumQueries(4):
-            tasks = agent.next_tasks()
-            self.assertEqual(len(tasks), 1)
-
-        # Build some inefficient agents
-        for i in range(1, 20):
-            self._build_agent(
-                hostname=f"agent_{i}",
-                cpu_cores=2,
-                ram_total=2e9,
-            )
-
-        self.assertEqual(Agent.objects.count(), 20)
-        with self.assertNumQueries(4):
-            tasks = agent.next_tasks()
-            self.assertEqual(len(tasks), 1)
-
-    def test_gpu_assignation(self):
-        """
-        Check a GPU enabled agent gets a GPU task
-        """
-        # Agent normal has 20 free "slots"
-        # It should get all the tasks
-        agent_normal = self._build_agent(
-            hostname="agent_normal", cpu_cores=20, ram_total=20e9
-        )
-
-        # But this one has a GPU: it will get GPU tasks in priority
-        agent_gpu = self._build_agent(hostname="agent_2", cpu_cores=10, ram_total=6e9)
-        agent_gpu.gpus.create(id=uuid.uuid4(), index=0, ram_total=8e9, name="Fake GPU")
-
-        # Add 6 normal + 1 GPU pending tasks to the system
-        tasks = self._add_pending_tasks(7)
-        task_gpu = tasks[-1]
-
-        # Require gpu on that task
-        task_gpu.command = "/usr/bin/nvidia-smi"
-        task_gpu.requires_gpu = True
-        task_gpu.save()
-
-        # Normal agent should eat up most of the normal tasks
-        tasks = agent_normal.next_tasks()
-        self.assertEqual(len(tasks), 5)
-        self.assertFalse(any(t.requires_gpu for t in tasks))
-        self.assertFalse(any(t.gpu for t in tasks))
-        self._run_tasks(tasks)
-
-        # GPU agent should then get the GPU task
-        tasks = agent_gpu.next_tasks()
-        self.assertEqual(len(tasks), 1)
-        self.assertTrue(all(t.requires_gpu for t in tasks))
-        self.assertTrue(all(t.gpu for t in tasks))
-        self._run_tasks(tasks)
-
-        # Consume last normal task
-        tasks = agent_normal.next_tasks()
-        self.assertEqual(len(tasks), 1)
-        self._run_tasks(tasks)
-
-        # No more tasks left
-        self.assertEqual(Task.objects.filter(state=State.Pending).count(), 0)
-
-    def test_gpu_with_final_tasks_available(self):
-        """
-        A GPU that is assigned to some final tasks should still be available
-        """
-        gpu_agent = self._build_agent(hostname="matrix", cpu_cores=99, ram_total=9e12)
-        gpu = gpu_agent.gpus.create(id=uuid.uuid4(), index=0, ram_total=16e12, name="My cool GPU")
-
-        # Create some final tasks that use this GPU
-        Task.objects.bulk_create(
-            Task(
-                depth=0,
-                run=0,
-                process=self.process,
-                requires_gpu=True,
-                slug=f"task{i}",
-                state=state,
-                gpu=gpu,
-            )
-            for i, state in enumerate(FINAL_STATES)
-        )
-
-        # Create another task that needs a GPU and should be assigned to this agent
-        new_task = self.process.tasks.create(
-            run=0,
-            depth=0,
-            slug="new_task",
-            requires_gpu=True,
-            state=State.Unscheduled,
-        )
-
-        self.assertListEqual(gpu_agent.next_tasks(), [new_task])
-
-        new_task.refresh_from_db()
-        # The agent and GPU are assigned in Agent.next_actions, not next_tasks
-        self.assertIsNone(new_task.agent)
-        self.assertIsNone(new_task.gpu)
-
-    def test_gpu_with_active_task_unavailable(self):
-        """
-        A GPU that is assigned to any non-final task should not be available
-        """
-        gpu_agent = self._build_agent(hostname="matrix", cpu_cores=99, ram_total=9e12)
-        gpu = gpu_agent.gpus.create(id=uuid.uuid4(), index=0, ram_total=16e12, name="My cool GPU")
-
-        for state in ACTIVE_STATES:
-            self.process.tasks.all().delete()
-            with self.subTest(state=state):
-                # Create a task that is running on this agent's GPU
-                self.process.tasks.create(
-                    run=0,
-                    depth=0,
-                    slug="current_task",
-                    requires_gpu=True,
-                    agent=gpu_agent,
-                    gpu=gpu,
-                    state=state,
-                )
-
-                # Create another task that needs a GPU
-                new_task = self.process.tasks.create(
-                    run=0,
-                    depth=0,
-                    slug="new_task",
-                    requires_gpu=True,
-                    state=State.Unscheduled,
-                )
-
-                self.assertListEqual(gpu_agent.next_tasks(), [])
-
-                new_task.refresh_from_db()
-                self.assertIsNone(new_task.agent)
-                self.assertIsNone(new_task.gpu)
-
-    def test_multiple_farm_task_assignation(self):
-        """
-        Distribute tasks depending on farm
-        """
-        # Create one big agent on the test farm
-        test_agent = self._build_agent(cpu_cores=20, ram_total=64e9)
-        self._add_pending_tasks(3)
-        # and 3 small agents on the corn farm with a capacity for one task each
-        corn_farm = Farm.objects.create(name="Corn farm")
-        corn_agent_1, corn_agent_2 = [
-            self._build_agent(
-                hostname=f"agent_{i}",
-                cpu_cores=2,
-                ram_total=2e9,
-                farm=corn_farm,
-            )
-            for i in range(1, 3)
-        ]
-
-        corn_process = corn_farm.processes.create(
-            mode=ProcessMode.Workers,
-            creator=self.superuser,
-            corpus=self.corpus,
-        )
-        tasks = self._add_pending_tasks(3, process=corn_process)
-
-        self.assertEqual(Task.objects.count(), 6)
-        self.assertEqual(
-            len([agent for agent in Agent.objects.all() if self._active_agent(agent)]),
-            3,
-        )
-
-        # All except one task in the corn farm will be distributed
-        tasks = test_agent.next_tasks()
-        self.assertEqual(len(tasks), 3)
-        self.assertEqual(
-            set([task.process.farm_id for task in tasks]), {self.farm.id}
-        )
-
-        corn_tasks_1 = corn_agent_1.next_tasks()
-        self.assertEqual(len(corn_tasks_1), 1)
-        corn_tasks_2 = corn_agent_2.next_tasks()
-        self.assertEqual(len(corn_tasks_2), 1)
-
-        self._run_tasks([*tasks, *corn_tasks_1, *corn_tasks_2])
-
-        # Update corn agents loads
-        Agent.objects.filter(farm=corn_farm).update(ram_load=0.95, cpu_load=1.9)
-
-        # No agent can retrieve the last pending task
-        self.assertEqual(Task.objects.filter(state=State.Pending).count(), 1)
-        for agent in Agent.objects.all():
-            self.assertEqual(len(agent.next_tasks()), 0)
-
-    def test_next_tasks_ordering(self):
-        agent_1 = self._build_agent(hostname="agent_1", cpu_cores=11, ram_total=20e9)
-        self.assertEqual(
-            [a.hostname for a in Agent.objects.all() if self._active_agent(a) is True],
-            ["agent_1"],
-        )
-        # Add 3 pending low priority tasks to the system
-        lp_tasks = self._add_pending_tasks(3, slug_ext="_lp", priority="1")
-        # Add 3 pending normal priority tasks to the system
-        np_tasks = self._add_pending_tasks(3, slug_ext="_np")
-        # Add 3 pending high priority tasks to the system
-        hp_tasks = self._add_pending_tasks(3, slug_ext="_hp", priority="100")
-
-        # As agents are registered, they should retrieve 3 tasks each
-        tasks = agent_1.next_tasks()
-        self.assertEqual(len(tasks), 9)
-        # First three tasks should be the high priority ones
-        self.assertEqual(tasks[:3], hp_tasks)
-        # Second three tasks should be the normal priority ones
-        self.assertEqual(tasks[3:6], np_tasks)
-        # Last three tasks should be the low priority ones
-        self.assertEqual(tasks[6:], lp_tasks)
-
-    def test_no_cpu_overload(self):
-        """A ponos agent should never run more tasks than its number of reported cores."""
-        # Agent 1 has 4 free cores
-        agent_1 = self._build_agent(hostname="agent_1", cpu_cores=4, ram_total=128e9, cpu_load=0.0)
-        # Agent 2 only has 2 out of 64 cores available
-        agent_2 = self._build_agent(hostname="agent_2", cpu_cores=64, ram_total=128e9, cpu_load=61.9)
-        self.assertEqual(
-            [a.hostname for a in Agent.objects.all() if self._active_agent(a) is True],
-            ["agent_1", "agent_2"],
-        )
-
-        tasks = self._add_pending_tasks(3)
-        # Agent 1 has 4 CPU slots
-        self.assertEqual(len(agent_1.next_tasks()), 3)
-        # Agent 2 has a huge load
-        self.assertEqual(len(agent_2.next_tasks()), 0)
-
-        # Assign those 3 tasks to agent 1, its real load is 10%
-        Task.objects.filter(id__in=[t.id for t in tasks]).update(agent=agent_1, state=State.Running)
-        agent_1.cpu_load = 0.4
-        agent_1.save()
-
-        self._add_pending_tasks(10, slug_ext="new")
-        # Agent 1 has a low load but 0 CPU slots according to the currently running tasks
-        self.assertEqual(len(agent_1.next_tasks()), 0)
-        # Agent 2 has a huge load but still 2 CPU slots
-        self.assertEqual(len(agent_2.next_tasks()), 2)
diff --git a/arkindex/ponos/utils.py b/arkindex/ponos/utils.py
index e20ad3fc11..eba004ebb5 100644
--- a/arkindex/ponos/utils.py
+++ b/arkindex/ponos/utils.py
@@ -1,3 +1,6 @@
+import os
+
+import magic
 from arkindex.ponos.models import Task
 
 
@@ -8,3 +11,14 @@ def is_admin_or_ponos_task(request):
 def get_process_from_task_auth(request):
     if isinstance(request.auth, Task):
         return request.auth.process
+
+
+def upload_artifact(task, path, artifacts_dir):
+    content_type = magic.from_file(path, mime=True)
+    size = os.path.getsize(path)
+    artifact = task.artifacts.create(
+        path=os.path.relpath(path, artifacts_dir),
+        content_type=content_type,
+        size=size,
+    )
+    artifact.s3_object.upload_file(str(path), ExtraArgs={"ContentType": content_type})
diff --git a/arkindex/process/admin.py b/arkindex/process/admin.py
index f02159a5c1..3349b5f270 100644
--- a/arkindex/process/admin.py
+++ b/arkindex/process/admin.py
@@ -13,7 +13,6 @@ from arkindex.process.models import (
     WorkerVersion,
 )
 from arkindex.project.admin import ArchivedListFilter
-from arkindex.users.admin import GroupMembershipInline, UserMembershipInline
 
 
 class DataFileInline(admin.StackedInline):
@@ -78,7 +77,7 @@ class RepositoryAdmin(admin.ModelAdmin):
     list_display = ("id", "url")
     fields = ("id", "url")
     readonly_fields = ("id", )
-    inlines = [WorkerInline, UserMembershipInline, GroupMembershipInline]
+    inlines = [WorkerInline, ]
 
 
 class WorkerVersionInline(admin.StackedInline):
@@ -106,7 +105,7 @@ class WorkerAdmin(admin.ModelAdmin):
     list_filter = (ArchivedListFilter, )
     fields = ("id", "name", "slug", "type", "description", "repository", "public", "archived")
     readonly_fields = ("id", )
-    inlines = [WorkerVersionInline, UserMembershipInline, GroupMembershipInline, WorkerConfigurationInline]
+    inlines = [WorkerVersionInline, WorkerConfigurationInline]
 
     def get_queryset(self, *args, **kwargs):
         return super().get_queryset(*args, **kwargs).select_related("repository", "type")
diff --git a/arkindex/process/api.py b/arkindex/process/api.py
index 13c1dfebb7..42cefcecb5 100644
--- a/arkindex/process/api.py
+++ b/arkindex/process/api.py
@@ -1076,7 +1076,7 @@ class CorpusWorkerVersionList(CorpusACLMixin, ListAPIView):
 
     @cached_property
     def corpus(self):
-        return get_object_or_404(self.readable_corpora, pk=self.kwargs["pk"])
+        return get_object_or_404(Corpus.objects.readable(self.request.user), pk=self.kwargs["pk"])
 
     def get_queryset(self):
         return (
@@ -1107,7 +1107,7 @@ class CorpusWorkerVersionList(CorpusACLMixin, ListAPIView):
         tags=["ml"],
     )
 )
-class CorpusWorkerRunList(CorpusACLMixin, ListAPIView):
+class CorpusWorkerRunList(ListAPIView):
     """
     List worker runs used by any ML result in a given corpus.
 
@@ -1126,7 +1126,7 @@ class CorpusWorkerRunList(CorpusACLMixin, ListAPIView):
 
     @cached_property
     def corpus(self):
-        return get_object_or_404(self.readable_corpora, pk=self.kwargs["pk"])
+        return get_object_or_404(Corpus.objects.readable(self.request.user), pk=self.kwargs["pk"])
 
     def get_queryset(self):
         return (
@@ -1145,7 +1145,7 @@ class CorpusWorkerRunList(CorpusACLMixin, ListAPIView):
                 ),
                 "process__tasks",
             )
-            .filter(Q(process__corpus_id=self.corpus.id) | Q(process__creator_id=self.user.id, process__mode=ProcessMode.Local), has_results=True)
+            .filter(Q(process__corpus_id=self.corpus.id) | Q(process__creator_id=self.request.user.id, process__mode=ProcessMode.Local), has_results=True)
             .annotate(process_element_count=Count("process__elements"))
             .order_by("summary")
         )
diff --git a/arkindex/process/builder.py b/arkindex/process/builder.py
index 33e7d7f649..dd3445337f 100644
--- a/arkindex/process/builder.py
+++ b/arkindex/process/builder.py
@@ -213,8 +213,6 @@ class ProcessBuilder(object):
         """
         from arkindex.process.models import ActivityState, ProcessMode
 
-        if not self.process.farm_id:
-            raise ValidationError("Process must have a farm ID to be built")
         if self.run > 0 and not self.process.is_final:
             raise ValidationError("Cannot run a process that is not in a final state.")
         if self.process.mode in (ProcessMode.Local, ProcessMode.Template):
@@ -393,3 +391,4 @@ class ProcessBuilder(object):
             for child_slug, parent_slugs in self.tasks_parents.items()
             for parent_slug in parent_slugs
         )
+        return tasks
diff --git a/arkindex/process/models.py b/arkindex/process/models.py
index ac26165f47..714e382c91 100644
--- a/arkindex/process/models.py
+++ b/arkindex/process/models.py
@@ -1,5 +1,6 @@
 import urllib.parse
 import uuid
+from functools import partial
 from typing import Optional
 
 from django.conf import settings
@@ -390,11 +391,16 @@ class Process(IndexableModel):
         """
         Build and start a new run for this process.
         """
+        from arkindex.project.triggers import schedule_tasks
+
         process_builder = ProcessBuilder(self)
         process_builder.validate()
         process_builder.build()
         # Save all tasks and their relations
         process_builder.save()
+        if settings.PONOS_RQ_EXECUTION:
+            # Trigger tasks execution in RQ after the current transaction so tasks have been created
+            transaction.on_commit(partial(schedule_tasks, process=self, run=process_builder.run))
 
         self.started = timezone.now()
         self.finished = None
diff --git a/arkindex/process/serializers/imports.py b/arkindex/process/serializers/imports.py
index b5a793d49d..a0688da6ca 100644
--- a/arkindex/process/serializers/imports.py
+++ b/arkindex/process/serializers/imports.py
@@ -2,12 +2,13 @@ from collections import defaultdict
 from textwrap import dedent
 
 from django.conf import settings
+from django.utils.module_loading import import_string
 from rest_framework import serializers
 from rest_framework.exceptions import PermissionDenied, ValidationError
 
 from arkindex.documents.models import Corpus, Element, ElementType, MLClass
 from arkindex.ponos.models import Farm, State
-from arkindex.ponos.serializers import FarmSerializer, TaskLightSerializer
+from arkindex.ponos.serializers import TaskLightSerializer
 from arkindex.process.models import (
     ActivityState,
     DataFile,
@@ -18,7 +19,6 @@ from arkindex.process.models import (
     WorkerVersionState,
 )
 from arkindex.process.serializers.git import RevisionSerializer
-from arkindex.process.utils import get_default_farm
 from arkindex.project.mixins import ProcessACLMixin
 from arkindex.project.serializer_fields import EnumField, LinearRingField
 from arkindex.project.validators import MaxValueValidator
@@ -26,6 +26,9 @@ from arkindex.training.models import ModelVersionState
 from arkindex.users.models import Role
 from arkindex.users.utils import get_max_level
 
+ProcessFarmField = import_string(getattr(settings, "PROCESS_FARM_FIELD", None) or "arkindex.project.serializer_fields.NullField")
+get_default_farm = import_string(getattr(settings, "GET_DEFAULT_FARM", None) or "arkindex.process.utils.get_default_farm")
+
 
 class ProcessLightSerializer(serializers.ModelSerializer):
     """
@@ -138,7 +141,7 @@ class ProcessSerializer(ProcessLightSerializer):
 
 
 class ProcessDetailsSerializer(ProcessSerializer):
-    farm = FarmSerializer(read_only=True)
+    farm = ProcessFarmField(read_only=True)
     tasks = TaskLightSerializer(many=True, read_only=True)
 
     class Meta(ProcessSerializer.Meta):
@@ -322,7 +325,7 @@ class FilesProcessSerializer(serializers.ModelSerializer):
         if farm is None:
             farm = get_default_farm()
 
-        if not farm.is_available(self.context["request"].user):
+        if farm and not farm.is_available(self.context["request"].user):
             raise ValidationError(["You do not have access to this farm."])
 
         return farm
@@ -406,7 +409,7 @@ class StartProcessSerializer(serializers.Serializer):
         if farm is None:
             farm = get_default_farm()
 
-        if not farm.is_available(self.context["request"].user):
+        if farm and not farm.is_available(self.context["request"].user):
             raise ValidationError(["You do not have access to this farm."])
 
         return farm
diff --git a/arkindex/process/serializers/ingest.py b/arkindex/process/serializers/ingest.py
index 77da4b8384..c888bb7ea5 100644
--- a/arkindex/process/serializers/ingest.py
+++ b/arkindex/process/serializers/ingest.py
@@ -86,7 +86,7 @@ class S3ImportSerializer(serializers.ModelSerializer):
         if farm is None:
             farm = get_default_farm()
 
-        if not farm.is_available(self.context["request"].user):
+        if farm and not farm.is_available(self.context["request"].user):
             raise serializers.ValidationError(["You do not have access to this farm."])
 
         return farm
diff --git a/arkindex/process/tests/test_corpus_worker_runs.py b/arkindex/process/tests/test_corpus_worker_runs.py
index 92aebf153a..2e62a89f37 100644
--- a/arkindex/process/tests/test_corpus_worker_runs.py
+++ b/arkindex/process/tests/test_corpus_worker_runs.py
@@ -1,3 +1,5 @@
+from unittest.mock import call, patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -80,16 +82,19 @@ class TestCorpusWorkerRuns(FixtureAPITestCase):
         cls.user_local_run.has_results = True
         cls.user_local_run.save()
 
-    def test_list_requires_read_access(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_list_requires_read_access(self, filter_rights_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.get(reverse("api:corpus-runs", kwargs={"pk": self.private_corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
 
     def test_list(self):
         self.private_corpus.memberships.create(user=self.user, level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(7):
             response = self.client.get(reverse("api:corpus-runs", kwargs={"pk": self.private_corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json()["results"], [
diff --git a/arkindex/process/tests/test_create_process.py b/arkindex/process/tests/test_create_process.py
index 008c963eaa..e15b55f994 100644
--- a/arkindex/process/tests/test_create_process.py
+++ b/arkindex/process/tests/test_create_process.py
@@ -181,7 +181,8 @@ class TestCreateProcess(FixtureAPITestCase):
             {"corpus": ["This field is required."]}
         )
 
-    def test_create_process_requires_corpus_admin(self):
+    @patch("arkindex.process.serializers.imports.get_max_level", return_value=Role.Contributor.value)
+    def test_create_process_requires_corpus_admin(self, get_max_level_mock):
         """
         Only an admin of the target corpus can create a workers process
         """
@@ -198,7 +199,11 @@ class TestCreateProcess(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"corpus": ["You do not have an admin access to this corpus."]})
 
-    def test_create_process_non_readable_corpus(self):
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.corpus))
+
+    @patch("arkindex.process.serializers.imports.get_max_level", return_value=None)
+    def test_create_process_non_readable_corpus(self, get_max_level_mock):
         self.client.force_login(self.user)
         response = self.client.post(
             reverse("api:corpus-process"),
@@ -211,6 +216,9 @@ class TestCreateProcess(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"corpus": ["Corpus with this ID does not exist."]})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.private_corpus))
+
     def test_create_process_name_filter(self):
         self.client.force_login(self.user)
         response = self.client.post(
@@ -337,7 +345,7 @@ class TestCreateProcess(FixtureAPITestCase):
 
     def test_ml_class_filter(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.post(
                 reverse("api:corpus-process"),
                 {
@@ -376,7 +384,7 @@ class TestCreateProcess(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
         element = self.corpus.elements.create(type=self.pages.first().type, name="Kill me please")
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:corpus-process"),
                 {
@@ -577,7 +585,7 @@ class TestCreateProcess(FixtureAPITestCase):
         self.assertFalse(self.corpus.worker_versions.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process_2.id)}),
                 {"worker_activity": True},
@@ -668,7 +676,7 @@ class TestCreateProcess(FixtureAPITestCase):
         )
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process_2.id)}),
                 {"use_cache": True},
@@ -716,7 +724,7 @@ class TestCreateProcess(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process_2.id)}),
                 {"use_gpu": True},
@@ -787,7 +795,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process.use_gpu = True
         process.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"use_gpu": "true"}
@@ -811,7 +819,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process.use_gpu = True
         process.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"use_gpu": "true"}
@@ -831,7 +839,7 @@ class TestCreateProcess(FixtureAPITestCase):
         ProcessDataset.objects.create(process=process, dataset=dataset, sets=dataset.sets)
         process.versions.set([self.version_2, self.version_3])
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(9):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -844,7 +852,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process = self.corpus.processes.create(creator=self.user, mode=ProcessMode.Workers)
         process.versions.set([self.version_2, self.version_3])
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(9):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -861,7 +869,7 @@ class TestCreateProcess(FixtureAPITestCase):
         ProcessDataset.objects.create(process=process, dataset=dataset, sets=dataset.sets)
         process.versions.add(self.version_1)
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(9):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -877,7 +885,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process = self.corpus.processes.create(creator=self.user, mode=ProcessMode.Workers)
         process.versions.add(self.version_1)
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(9):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -899,7 +907,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process = self.corpus.processes.create(creator=self.user, mode=ProcessMode.Workers)
         process.versions.add(custom_version)
 
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -936,7 +944,7 @@ class TestCreateProcess(FixtureAPITestCase):
             }
             if mode:
                 data["mode"] = mode
-            with (self.subTest(mode=mode), self.assertNumQueries(11)):
+            with (self.subTest(mode=mode), self.assertNumQueries(8)):
                 response = self.client.post(
                     reverse("api:corpus-process"),
                     data,
@@ -968,7 +976,7 @@ class TestCreateProcess(FixtureAPITestCase):
 
     def test_create_process_dataset_mode_no_selection(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:corpus-process"),
                 {
@@ -989,7 +997,7 @@ class TestCreateProcess(FixtureAPITestCase):
         self.client.force_login(self.user)
         cases = [item.value for item in ProcessMode if item.value not in ["workers", "dataset"]]
         for mode in cases:
-            with (self.subTest(mode=mode), self.assertNumQueries(9)):
+            with (self.subTest(mode=mode), self.assertNumQueries(6)):
                 response = self.client.post(
                     reverse("api:corpus-process"),
                     {
diff --git a/arkindex/process/tests/test_create_s3_import.py b/arkindex/process/tests/test_create_s3_import.py
index 454f3282c9..5d95c18a68 100644
--- a/arkindex/process/tests/test_create_s3_import.py
+++ b/arkindex/process/tests/test_create_s3_import.py
@@ -1,3 +1,6 @@
+from unittest import expectedFailure
+from unittest.mock import call, patch
+
 from django.test import override_settings
 from django.urls import reverse
 from rest_framework import status
@@ -18,7 +21,6 @@ class TestCreateS3Import(FixtureTestCase):
         cls.import_worker_version = WorkerVersion.objects.get(worker__slug="file_import")
         cls.default_farm = Farm.objects.create(name="Crypto farm")
         cls.default_farm.memberships.create(user=cls.user, level=Role.Guest.value)
-        cls.other_farm = Farm.objects.get(name="Wheat farm")
 
     def test_requires_login(self):
         with self.assertNumQueries(0):
@@ -59,7 +61,9 @@ class TestCreateS3Import(FixtureTestCase):
             })
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_invalid(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_invalid(self, filter_rights_mock):
+        filter_rights_mock.return_value = Corpus.objects.filter(id=self.corpus.id)
         self.user.user_scopes.create(scope=Scope.S3Ingest)
         self.client.force_login(self.user)
         private_corpus = Corpus.objects.create(name="nope")
@@ -111,10 +115,13 @@ class TestCreateS3Import(FixtureTestCase):
             ),
         ]
         for request, expected in cases:
+            filter_rights_mock.reset_mock()
             with self.subTest(request=request):
                 response = self.client.post(reverse("api:s3-import-create"), request)
                 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
                 self.assertDictEqual(response.json(), expected)
+                self.assertEqual(filter_rights_mock.call_count, 1)
+                self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Admin.value))
 
     @override_settings(
         PONOS_DEFAULT_ENV={},
@@ -132,7 +139,7 @@ class TestCreateS3Import(FixtureTestCase):
         ImageServer.objects.create(id=999, display_name="Ingest image server", url="https://dev.null.teklia.com")
         element = self.corpus.elements.get(name="Volume 1")
 
-        with self.assertNumQueries(27), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
+        with self.assertNumQueries(22), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
             response = self.client.post(reverse("api:s3-import-create"), {
                 "corpus_id": str(self.corpus.id),
                 "element_id": str(element.id),
@@ -140,7 +147,6 @@ class TestCreateS3Import(FixtureTestCase):
                 "element_type": "page",
                 "bucket_name": "blah",
                 "prefix": "a/b/c",
-                "farm_id": str(self.other_farm.id),
             })
             self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json())
         data = response.json()
@@ -196,7 +202,7 @@ class TestCreateS3Import(FixtureTestCase):
         self.corpus.types.create(slug="folder", display_name="Folder", folder=True)
         ImageServer.objects.create(id=999, display_name="Ingest image server", url="https://dev.null.teklia.com")
 
-        with self.assertNumQueries(26), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
+        with self.assertNumQueries(21), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
             response = self.client.post(reverse("api:s3-import-create"), {
                 "corpus_id": str(self.corpus.id),
                 "bucket_name": "blah",
@@ -208,7 +214,7 @@ class TestCreateS3Import(FixtureTestCase):
         self.assertDictEqual(data, {"id": str(process.id)})
         self.assertEqual(process.mode, ProcessMode.S3)
         self.assertEqual(process.creator, self.user)
-        self.assertEqual(process.farm, self.default_farm)
+        self.assertEqual(process.farm, None)
         self.assertEqual(process.corpus_id, self.corpus.id)
         self.assertIsNone(process.element_id)
         self.assertEqual(process.folder_type, self.corpus.types.get(slug="folder"))
@@ -239,19 +245,19 @@ class TestCreateS3Import(FixtureTestCase):
             "INGEST_S3_SECRET_KEY": "its-secret-i-wont-tell-you",
         })
 
-    def test_farm_guest(self):
+    @expectedFailure
+    @override_settings(INGEST_IMAGESERVER_ID=999)
+    @patch("arkindex.users.utils.get_max_level", return_value=None)
+    def test_farm_guest(self, get_max_level_mock):
         self.user.user_scopes.create(scope=Scope.S3Ingest)
         self.client.force_login(self.user)
         self.corpus.types.create(slug="folder", display_name="Folder", folder=True)
         ImageServer.objects.create(id=999, display_name="Ingest image server", url="https://dev.null.teklia.com")
 
-        self.other_farm.memberships.filter(user=self.user).delete()
-
-        with self.assertNumQueries(8), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
+        with self.assertNumQueries(5), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
             response = self.client.post(reverse("api:s3-import-create"), {
                 "corpus_id": str(self.corpus.id),
                 "bucket_name": "blah",
-                "farm_id": str(self.other_farm.id),
             })
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -260,7 +266,12 @@ class TestCreateS3Import(FixtureTestCase):
         })
         self.assertFalse(Process.objects.filter(mode=ProcessMode.S3).exists())
 
-    def test_default_farm_guest(self):
+        self.assertEqual(get_max_level_mock.call_count, 1)
+
+    @expectedFailure
+    @override_settings(INGEST_IMAGESERVER_ID=999)
+    @patch("arkindex.users.utils.get_max_level", return_value=None)
+    def test_default_farm_guest(self, get_max_level_mock):
         self.user.user_scopes.create(scope=Scope.S3Ingest)
         self.client.force_login(self.user)
         self.corpus.types.create(slug="folder", display_name="Folder", folder=True)
@@ -268,7 +279,7 @@ class TestCreateS3Import(FixtureTestCase):
 
         self.default_farm.memberships.filter(user=self.user).delete()
 
-        with self.assertNumQueries(8), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
+        with self.assertNumQueries(5), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
             response = self.client.post(reverse("api:s3-import-create"), {
                 "corpus_id": str(self.corpus.id),
                 "bucket_name": "blah",
@@ -279,3 +290,6 @@ class TestCreateS3Import(FixtureTestCase):
             "farm_id": ["You do not have access to this farm."],
         })
         self.assertFalse(Process.objects.filter(mode=ProcessMode.S3).exists())
+
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.default_farm))
diff --git a/arkindex/process/tests/test_datafile_api.py b/arkindex/process/tests/test_datafile_api.py
index 2d673ad00c..97b722c588 100644
--- a/arkindex/process/tests/test_datafile_api.py
+++ b/arkindex/process/tests/test_datafile_api.py
@@ -39,7 +39,8 @@ class TestDataFileApi(FixtureAPITestCase):
         response = self.client.post(reverse("api:file-create"), request)
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_create_df_corpus_acl(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_create_df_corpus_acl(self, filter_rights_mock):
         self.client.force_login(self.user)
         private_corpus = Corpus.objects.create(name="private")
         request = self.build_file_create_request(corpus=str(private_corpus.id))
diff --git a/arkindex/process/tests/test_process_datasets.py b/arkindex/process/tests/test_process_datasets.py
index 4efcd2af60..af98d7b253 100644
--- a/arkindex/process/tests/test_process_datasets.py
+++ b/arkindex/process/tests/test_process_datasets.py
@@ -1,6 +1,6 @@
 import uuid
 from datetime import datetime, timezone
-from unittest.mock import patch
+from unittest.mock import call, patch
 
 from django.urls import reverse
 from rest_framework import status
@@ -69,17 +69,21 @@ class TestProcessDatasets(FixtureAPITestCase):
             response = self.client.get(reverse("api:process-datasets", kwargs={"pk": str(uuid.uuid4())}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_list_process_access_level(self):
+    @patch("arkindex.project.mixins.get_max_level", return_value=None)
+    def test_list_process_access_level(self, get_max_level_mock):
         self.private_corpus.memberships.filter(user=self.test_user).delete()
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:process-datasets", kwargs={"pk": self.dataset_process.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have guest access to this process."})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.test_user, self.private_corpus))
+
     def test_list(self):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:process-datasets", kwargs={"pk": self.dataset_process.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json()["results"], [
@@ -139,22 +143,27 @@ class TestProcessDatasets(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_create_access_level(self):
-        cases = [None, Role.Guest, Role.Contributor]
+    @patch("arkindex.project.mixins.get_max_level")
+    def test_create_access_level(self, get_max_level_mock):
+        cases = [None, Role.Guest.value, Role.Contributor.value]
         for level in cases:
             with self.subTest(level=level):
-                self.private_corpus.memberships.filter(user=self.test_user).delete()
-                if level:
-                    self.private_corpus.memberships.create(user=self.test_user, level=level.value)
+                get_max_level_mock.reset_mock()
+                get_max_level_mock.return_value = level
                 self.client.force_login(self.test_user)
-                with self.assertNumQueries(5):
+
+                with self.assertNumQueries(3):
                     response = self.client.post(
                         reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset2.id}),
                         data={"sets": self.dataset2.sets}
                     )
                     self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
                 self.assertEqual(response.json(), {"detail": "You do not have admin access to this process."})
 
+                self.assertEqual(get_max_level_mock.call_count, 1)
+                self.assertEqual(get_max_level_mock.call_args, call(self.test_user, self.private_corpus))
+
     def test_create_process_mode(self):
         cases = set(ProcessMode) - {ProcessMode.Dataset, ProcessMode.Local, ProcessMode.Repository}
         for mode in cases:
@@ -163,7 +172,7 @@ class TestProcessDatasets(FixtureAPITestCase):
                 self.dataset_process.save()
                 self.client.force_login(self.test_user)
 
-                with self.assertNumQueries(8):
+                with self.assertNumQueries(5):
                     response = self.client.post(
                         reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset2.id}),
                         data={"sets": self.dataset2.sets}
@@ -183,17 +192,6 @@ class TestProcessDatasets(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {"detail": "You do not have admin access to this process."})
 
-    def test_create_process_mode_repository(self):
-        self.client.force_login(self.user)
-        process = Process.objects.create(creator=self.user, mode=ProcessMode.Repository, revision=self.rev)
-        with self.assertNumQueries(6):
-            response = self.client.post(
-                reverse("api:process-dataset", kwargs={"process": process.id, "dataset": self.dataset2.id}),
-                data={"sets": self.dataset2.sets}
-            )
-            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertEqual(response.json(), {"detail": "You do not have admin access to this process."})
-
     def test_create_wrong_process_uuid(self):
         self.client.force_login(self.test_user)
         wrong_id = uuid.uuid4()
@@ -208,7 +206,7 @@ class TestProcessDatasets(FixtureAPITestCase):
     def test_create_wrong_dataset_uuid(self):
         self.client.force_login(self.test_user)
         wrong_id = uuid.uuid4()
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": wrong_id}),
                 data={"sets": ["test"]}
@@ -216,23 +214,29 @@ class TestProcessDatasets(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(response.json(), {"dataset": [f'Invalid pk "{str(wrong_id)}" - object does not exist.']})
 
-    def test_create_dataset_access(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_create_dataset_access(self, filter_rights_mock):
         new_corpus = Corpus.objects.create(name="NERV")
         new_dataset = new_corpus.datasets.create(name="Eva series", description="We created the Evas from Adam", creator=self.user)
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(7):
+
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": new_dataset.id}),
                 data={"sets": new_dataset.sets}
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertEqual(response.json(), {"dataset": [f'Invalid pk "{str(new_dataset.id)}" - object does not exist.']})
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.test_user, Corpus, Role.Guest.value))
+
     def test_create_unique(self):
         self.client.force_login(self.test_user)
         self.assertTrue(self.dataset_process.datasets.filter(id=self.dataset1.id).exists())
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": self.dataset1.sets}
@@ -245,7 +249,7 @@ class TestProcessDatasets(FixtureAPITestCase):
         self.client.force_login(self.test_user)
         self.dataset_process.tasks.create(run=0, depth=0, slug="makrout")
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset2.id}),
                 data={"sets": self.dataset2.sets}
@@ -258,7 +262,7 @@ class TestProcessDatasets(FixtureAPITestCase):
         self.client.force_login(self.test_user)
         self.assertEqual(ProcessDataset.objects.count(), 3)
         self.assertFalse(ProcessDataset.objects.filter(process=self.dataset_process.id, dataset=self.dataset2.id).exists())
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse(
                     "api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset2.id}
@@ -295,7 +299,7 @@ class TestProcessDatasets(FixtureAPITestCase):
         self.client.force_login(self.test_user)
         self.assertEqual(ProcessDataset.objects.count(), 3)
         self.assertFalse(ProcessDataset.objects.filter(process=self.dataset_process.id, dataset=self.dataset2.id).exists())
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset2.id}),
                 data={"sets": ["validation", "test"]}
@@ -331,7 +335,7 @@ class TestProcessDatasets(FixtureAPITestCase):
         self.client.force_login(self.test_user)
         self.assertEqual(ProcessDataset.objects.count(), 3)
         self.assertFalse(ProcessDataset.objects.filter(process=self.dataset_process.id, dataset=self.dataset2.id).exists())
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset2.id}),
                 data={"sets": ["Unit-01"]}
@@ -368,13 +372,12 @@ class TestProcessDatasets(FixtureAPITestCase):
                 if level:
                     self.private_corpus.memberships.create(user=self.test_user, level=level.value)
                 self.client.force_login(self.test_user)
-                with self.assertNumQueries(5):
+                with self.assertNumQueries(4):
                     response = self.client.put(
                         reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                         data={"sets": ["test"]}
                     )
-                    self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-                self.assertEqual(response.json(), {"detail": "You do not have admin access to this process."})
+                    self.assertEqual(response.status_code, status.HTTP_200_OK)
 
     def test_update_process_does_not_exist(self):
         self.client.force_login(self.test_user)
@@ -405,7 +408,7 @@ class TestProcessDatasets(FixtureAPITestCase):
 
     def test_update(self):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": ["test"]}
@@ -431,7 +434,7 @@ class TestProcessDatasets(FixtureAPITestCase):
 
     def test_update_wrong_sets(self):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": ["Unit-01", "Unit-02"]}
@@ -441,7 +444,7 @@ class TestProcessDatasets(FixtureAPITestCase):
 
     def test_update_unique_sets(self):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": ["test", "test"]}
@@ -457,7 +460,7 @@ class TestProcessDatasets(FixtureAPITestCase):
             expiry=datetime(1970, 1, 1, tzinfo=timezone.utc),
         )
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": ["test"]}
@@ -470,7 +473,7 @@ class TestProcessDatasets(FixtureAPITestCase):
         Non "sets" fields in the update request are ignored
         """
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"process": str(self.dataset_process_2.id), "dataset": str(self.dataset2.id), "sets": ["test"]}
@@ -522,13 +525,12 @@ class TestProcessDatasets(FixtureAPITestCase):
                 if level:
                     self.private_corpus.memberships.create(user=self.test_user, level=level.value)
                 self.client.force_login(self.test_user)
-                with self.assertNumQueries(5):
+                with self.assertNumQueries(4):
                     response = self.client.patch(
                         reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                         data={"sets": ["test"]}
                     )
-                    self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-                self.assertEqual(response.json(), {"detail": "You do not have admin access to this process."})
+                    self.assertEqual(response.status_code, status.HTTP_200_OK)
 
     def test_partial_update_process_does_not_exist(self):
         self.client.force_login(self.test_user)
@@ -559,7 +561,7 @@ class TestProcessDatasets(FixtureAPITestCase):
 
     def test_partial_update(self):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": ["test"]}
@@ -585,7 +587,7 @@ class TestProcessDatasets(FixtureAPITestCase):
 
     def test_partial_update_wrong_sets(self):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": ["Unit-01", "Unit-02"]}
@@ -595,7 +597,7 @@ class TestProcessDatasets(FixtureAPITestCase):
 
     def test_partial_update_unique_sets(self):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": ["test", "test"]}
@@ -611,7 +613,7 @@ class TestProcessDatasets(FixtureAPITestCase):
             expiry=datetime(1970, 1, 1, tzinfo=timezone.utc),
         )
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"sets": ["test"]}
@@ -624,7 +626,7 @@ class TestProcessDatasets(FixtureAPITestCase):
         Non "sets" fields in the partial update request are ignored
         """
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
                 data={"process": str(self.dataset_process_2.id), "dataset": str(self.dataset2.id), "sets": ["test"]}
@@ -670,7 +672,7 @@ class TestProcessDatasets(FixtureAPITestCase):
     def test_destroy_dataset_does_not_exist(self):
         self.client.force_login(self.test_user)
         wrong_id = uuid.uuid4()
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": wrong_id})
             )
@@ -680,29 +682,33 @@ class TestProcessDatasets(FixtureAPITestCase):
     def test_destroy_not_found(self):
         self.assertFalse(self.dataset_process.datasets.filter(id=self.dataset2.id).exists())
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.delete(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset2.id}),
             )
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_destroy_process_access_level(self):
-        self.private_corpus.memberships.filter(user=self.test_user).delete()
+    @patch("arkindex.project.mixins.get_max_level", return_value=None)
+    def test_destroy_process_access_level(self, get_max_level_mock):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.private_dataset.id})
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have admin access to this process."})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.test_user, self.private_corpus))
+
     def test_destroy_no_dataset_access_requirement(self):
         new_corpus = Corpus.objects.create(name="NERV")
         new_dataset = new_corpus.datasets.create(name="Eva series", description="We created the Evas from Adam", creator=self.user)
         ProcessDataset.objects.create(process=self.dataset_process, dataset=new_dataset, sets=new_dataset.sets)
         self.assertTrue(ProcessDataset.objects.filter(process=self.dataset_process, dataset=new_dataset).exists())
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": new_dataset.id}),
             )
@@ -717,7 +723,7 @@ class TestProcessDatasets(FixtureAPITestCase):
                 self.dataset_process.save()
                 self.client.force_login(self.test_user)
 
-                with self.assertNumQueries(7):
+                with self.assertNumQueries(4):
                     response = self.client.delete(
                         reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset2.id}),
                     )
@@ -735,21 +741,11 @@ class TestProcessDatasets(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {"detail": "You do not have admin access to this process."})
 
-    def test_destroy_process_mode_repository(self):
-        self.client.force_login(self.user)
-        process = Process.objects.create(creator=self.user, mode=ProcessMode.Repository, revision=self.rev)
-        with self.assertNumQueries(6):
-            response = self.client.delete(
-                reverse("api:process-dataset", kwargs={"process": process.id, "dataset": self.dataset2.id}),
-            )
-            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertEqual(response.json(), {"detail": "You do not have admin access to this process."})
-
     def test_destroy_started(self):
         self.client.force_login(self.test_user)
         self.dataset_process.tasks.create(run=0, depth=0, slug="makrout")
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
             )
@@ -759,7 +755,7 @@ class TestProcessDatasets(FixtureAPITestCase):
 
     def test_destroy(self):
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
             )
@@ -774,7 +770,7 @@ class TestProcessDatasets(FixtureAPITestCase):
         self.process_dataset_1.sets = ["test"]
         self.process_dataset_1.save()
         self.client.force_login(self.test_user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:process-dataset", kwargs={"process": self.dataset_process.id, "dataset": self.dataset1.id}),
             )
diff --git a/arkindex/process/tests/test_process_elements.py b/arkindex/process/tests/test_process_elements.py
index 89d0e44de1..e437584d2d 100644
--- a/arkindex/process/tests/test_process_elements.py
+++ b/arkindex/process/tests/test_process_elements.py
@@ -1,4 +1,5 @@
 import uuid
+from unittest.mock import call, patch
 
 from django.urls import reverse
 from rest_framework import status
@@ -7,7 +8,7 @@ from arkindex.documents.models import Corpus, Element, ElementPath
 from arkindex.images.models import Image
 from arkindex.process.models import Process, ProcessMode
 from arkindex.project.tests import FixtureAPITestCase
-from arkindex.users.models import User
+from arkindex.users.models import Role
 
 
 class TestProcessElements(FixtureAPITestCase):
@@ -158,15 +159,19 @@ class TestProcessElements(FixtureAPITestCase):
         response = self.client.get(reverse("api:process-elements-list", kwargs={"pk": str(uuid.uuid4())}))
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_no_access(self):
-        self.process.corpus = Corpus.objects.create(name="private")
-        self.corpus.creator = User.objects.create_user("John Doe")
-        self.process.save()
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_no_access(self, has_access_mock):
         self.client.force_login(self.user)
-        response = self.client.get(reverse("api:process-elements-list", kwargs={"pk": self.process.id}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+        with self.assertNumQueries(4):
+            response = self.client.get(reverse("api:process-elements-list", kwargs={"pk": self.process.id}))
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have an admin access to the corpus of this process."})
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Admin.value, skip_public=False))
+
     def test_filter_elements_wrong_corpus(self):
         """
         Selected elements must be part of the same corpus as the process
diff --git a/arkindex/process/tests/test_processes.py b/arkindex/process/tests/test_processes.py
index d8bdb5cef8..38d06536e2 100644
--- a/arkindex/process/tests/test_processes.py
+++ b/arkindex/process/tests/test_processes.py
@@ -1,4 +1,5 @@
 import uuid
+from unittest import expectedFailure
 from unittest.mock import call, patch
 
 from django.conf import settings
@@ -36,10 +37,6 @@ class TestProcesses(FixtureAPITestCase):
     @classmethod
     def setUpTestData(cls):
         super().setUpTestData()
-        # The default farm will be the first one in alphabetical order
-        cls.default_farm = Farm.objects.create(name="Corn farm")
-        cls.default_farm.memberships.create(user=cls.user, level=Role.Guest.value)
-        cls.other_farm = Farm.objects.get(name="Wheat farm")
         cls.repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
         cls.rev = cls.repo.revisions.get()
         cls.dataset1, cls.dataset2 = Dataset.objects.filter(corpus=cls.corpus).order_by("name")
@@ -86,7 +83,6 @@ class TestProcesses(FixtureAPITestCase):
             mode=ProcessMode.Files,
             creator=cls.user,
             corpus=cls.private_corpus,
-            farm=cls.default_farm,
         )
         cls.private_ml_class = cls.private_corpus.ml_classes.create(name="beignet")
 
@@ -96,13 +92,11 @@ class TestProcesses(FixtureAPITestCase):
             mode=ProcessMode.Repository,
             creator=cls.user2,
             revision=cls.rev,
-            farm=cls.default_farm,
         )
         # Admin access
         cls.elts_process = cls.corpus.processes.create(
             mode=ProcessMode.Workers,
             creator=cls.user2,
-            farm=cls.default_farm,
         )
         cls.processes = Process.objects.filter(
             id__in=[cls.user_img_process.id, cls.repository_process.id, cls.elts_process.id]
@@ -137,7 +131,7 @@ class TestProcesses(FixtureAPITestCase):
         task.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:process-list"))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -219,7 +213,7 @@ class TestProcesses(FixtureAPITestCase):
         self.user_img_process.activity_state = ActivityState.Ready
         self.user_img_process.save()
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:process-list"), {"started": "true"})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -254,7 +248,7 @@ class TestProcesses(FixtureAPITestCase):
         self.elts_process.run()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:process-list"), {"started": "false"})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -321,7 +315,7 @@ class TestProcesses(FixtureAPITestCase):
         self.client.force_login(self.user)
         assert Process.objects.exclude(mode=ProcessMode.Files).exists()
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:process-list"), {"mode": ProcessMode.Files.value, "started": False})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -335,7 +329,7 @@ class TestProcesses(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:process-list"), {"mode": ProcessMode.Local.value})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -377,7 +371,7 @@ class TestProcesses(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:process-list"), {"created": "true"})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -424,7 +418,7 @@ class TestProcesses(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:process-list"), {"created": "false"})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -449,7 +443,7 @@ class TestProcesses(FixtureAPITestCase):
             mode=ProcessMode.Files,
         )
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:process-list"), {"id": process_id[:10], "started": False})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -472,7 +466,7 @@ class TestProcesses(FixtureAPITestCase):
             name="Numero Duo"
         )
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:process-list"), {"name": "Numb", "started": False})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -510,7 +504,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(stopped_process.state, State.Stopped)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:process-list"), {"state": "completed"})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -539,7 +533,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(stopped_process_2.state, State.Stopped)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:process-list"), {"state": "stopped"})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -571,7 +565,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(self.elts_process.state, State.Unscheduled)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:process-list"), {"state": "unscheduled", "started": True})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -682,7 +676,7 @@ class TestProcesses(FixtureAPITestCase):
             self.user_img_process.run()
         task = self.user_img_process.tasks.get()
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.get(reverse("api:process-details", kwargs={"pk": self.user_img_process.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -692,10 +686,7 @@ class TestProcesses(FixtureAPITestCase):
             "element": None,
             "element_name_contains": None,
             "element_type": None,
-            "farm": {
-                "id": str(self.default_farm.id),
-                "name": "Corn farm",
-            },
+            "farm": None,
             "files": [],
             "folder_type": None,
             "id": str(self.user_img_process.id),
@@ -763,18 +754,10 @@ class TestProcesses(FixtureAPITestCase):
             {"__all__": ["Please wait activities to be initialized before deleting this process"]}
         )
 
-    def test_delete_repository_import_no_permission(self):
-        """
-        Deletion of a repository import requires to be admin on the repository
-        """
-        self.client.force_login(self.user)
-        response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.repository_process.id}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {"detail": "You do not have a sufficient access level to this process."})
-
-    def test_delete_corpus_import_no_permission(self):
+    @expectedFailure
+    def test_delete_process_no_permission(self):
         """
-        A user cannot delete a process linked to a corpus he has no admin access to
+        A user cannot delete a process linked to a corpus they have no admin access to
         """
         self.client.force_login(self.user)
         self.assertFalse(self.user_img_process.corpus.memberships.filter(user=self.user).exists())
@@ -820,7 +803,7 @@ class TestProcesses(FixtureAPITestCase):
         If no activities exists for this process, it is deleted directly.
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(22):
+        with self.assertNumQueries(19):
             response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.elts_process.id}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         with self.assertRaises(Process.DoesNotExist):
@@ -847,7 +830,7 @@ class TestProcesses(FixtureAPITestCase):
             element=self.corpus.elements.get(name="Volume 1"),
             worker_version=WorkerVersion.objects.get(worker__slug="reco"),
         )
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(11):
             response = self.client.delete(reverse("api:process-details", kwargs={"pk": self.elts_process.id}))
             self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
         self.assertEqual(delay_mock.call_count, 1)
@@ -906,7 +889,7 @@ class TestProcesses(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.elts_process.run()
         self.elts_process.tasks.update(state=State.Running)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                 {"element_name_contains": "something"},
@@ -923,7 +906,7 @@ class TestProcesses(FixtureAPITestCase):
         self.elts_process.run()
         self.elts_process.tasks.update(state=State.Running)
         self.assertEqual(self.elts_process.name, None)
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(11):
             response = self.client.patch(
                 reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                 {"name": "newName"},
@@ -942,7 +925,6 @@ class TestProcesses(FixtureAPITestCase):
         process = self.corpus.processes.create(
             mode=ProcessMode.Files,
             creator=self.user,
-            farm=self.default_farm,
         )
         with self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
             process.run()
@@ -952,7 +934,7 @@ class TestProcesses(FixtureAPITestCase):
         # Take any element with an image: this will cause extra queries to retrieve the Image and ImageServer
         element = self.corpus.elements.exclude(image_id=None).first()
 
-        with self.assertNumQueries(15), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
+        with self.assertNumQueries(12), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
             response = self.client.patch(
                 reverse("api:process-details", kwargs={"pk": process.id}),
                 {"element_id": str(element.id)},
@@ -963,6 +945,7 @@ class TestProcesses(FixtureAPITestCase):
         process.refresh_from_db()
         self.assertEqual(process.element, element)
 
+    @expectedFailure
     def test_partial_update_no_permission(self):
         """
         A user cannot update a process linked to a corpus he has no admin access to
@@ -973,6 +956,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have a sufficient access level to this process."})
 
+    @expectedFailure
     def test_partial_update_repository_requires_admin(self):
         """
         Edition of a repository import requires to be admin on the repository
@@ -998,7 +982,7 @@ class TestProcesses(FixtureAPITestCase):
                 self.elts_process.refresh_from_db()
                 self.assertEqual(self.elts_process.state, state)
 
-                with self.assertNumQueries(16):
+                with self.assertNumQueries(13):
                     response = self.client.patch(
                         reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                         {"state": "stopping"},
@@ -1018,7 +1002,7 @@ class TestProcesses(FixtureAPITestCase):
         self.elts_process.run()
         self.assertEqual(self.elts_process.state, State.Unscheduled)
 
-        with self.assertNumQueries(17):
+        with self.assertNumQueries(14):
             response = self.client.patch(
                 reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                 {"state": "stopping"},
@@ -1042,7 +1026,7 @@ class TestProcesses(FixtureAPITestCase):
 
         for state in set(State) - {State.Stopping}:
             with self.subTest(state=state):
-                with self.assertNumQueries(9):
+                with self.assertNumQueries(6):
                     response = self.client.patch(
                         reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                         {"state": state.value},
@@ -1070,7 +1054,7 @@ class TestProcesses(FixtureAPITestCase):
                 self.elts_process.refresh_from_db()
                 self.assertEqual(self.elts_process.state, state)
 
-                with self.assertNumQueries(9):
+                with self.assertNumQueries(6):
                     response = self.client.patch(
                         reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                         {"state": "stopping"},
@@ -1159,7 +1143,7 @@ class TestProcesses(FixtureAPITestCase):
         self.elts_process.run()
         self.assertEqual(self.elts_process.state, State.Unscheduled)
 
-        with self.assertNumQueries(18):
+        with self.assertNumQueries(15):
             response = self.client.put(
                 reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                 {
@@ -1190,7 +1174,7 @@ class TestProcesses(FixtureAPITestCase):
 
         for state in set(State) - {State.Stopping}:
             with self.subTest(state=state):
-                with self.assertNumQueries(10):
+                with self.assertNumQueries(7):
                     response = self.client.put(
                         reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                         {
@@ -1225,7 +1209,7 @@ class TestProcesses(FixtureAPITestCase):
                 self.elts_process.refresh_from_db()
                 self.assertEqual(self.elts_process.state, state)
 
-                with self.assertNumQueries(10):
+                with self.assertNumQueries(7):
                     response = self.client.put(
                         reverse("api:process-details", kwargs={"pk": self.elts_process.id}),
                         {
@@ -1259,7 +1243,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertIsNone(process.ml_class)
         self.assertFalse(process.load_children)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.put(
                 reverse("api:process-details", kwargs={"pk": process.id}),
                 {
@@ -1287,9 +1271,9 @@ class TestProcesses(FixtureAPITestCase):
 
         non_existent_id = uuid.uuid4()
         cases = [
-            ("je says pas", "“je says pas” is not a valid UUID.", 9),
-            (non_existent_id, f'Invalid pk "{non_existent_id}" - object does not exist.', 10),
-            (self.private_ml_class.id, f'Invalid pk "{self.private_ml_class.id}" - object does not exist.', 10),
+            ("je says pas", "“je says pas” is not a valid UUID.", 6),
+            (non_existent_id, f'Invalid pk "{non_existent_id}" - object does not exist.', 7),
+            (self.private_ml_class.id, f'Invalid pk "{self.private_ml_class.id}" - object does not exist.', 7),
         ]
         for ml_class_id, message, queries in cases:
             with self.subTest(ml_class_id=ml_class_id), self.assertNumQueries(queries):
@@ -1318,7 +1302,7 @@ class TestProcesses(FixtureAPITestCase):
         self.client.force_login(self.user)
         process = Process.objects.create(mode=ProcessMode.Workers, corpus=self.corpus, creator=self.user)
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.put(
                 reverse("api:process-details", kwargs={"pk": process.id}),
                 {
@@ -1352,6 +1336,7 @@ class TestProcesses(FixtureAPITestCase):
             "name": "newName",
             "element_name_contains": "AAA",
             "element_type": "page",
+            "farm": None,
             "folder_type": None,
             "ml_class_id": None,
             "load_children": True,
@@ -1366,7 +1351,6 @@ class TestProcesses(FixtureAPITestCase):
             "revision": None,
             "state": "unscheduled",
             "template_id": None,
-            "farm": None,
             "tasks": [],
             "created": process.created.isoformat().replace("+00:00", "Z"),
             "updated": process.updated.isoformat().replace("+00:00", "Z"),
@@ -1442,6 +1426,7 @@ class TestProcesses(FixtureAPITestCase):
                 process.refresh_from_db()
                 self.assertEqual(process.name, "newName")
 
+    @expectedFailure
     def test_partial_update_corpus_no_write_right(self):
         self.client.force_login(self.user)
         self.corpus.memberships.filter(user=self.user).update(level=Role.Guest.value)
@@ -1461,7 +1446,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertIsNone(process.ml_class)
         self.assertFalse(process.load_children)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.patch(
                 reverse("api:process-details", kwargs={"pk": process.id}),
                 {
@@ -1530,9 +1515,9 @@ class TestProcesses(FixtureAPITestCase):
 
         non_existent_id = uuid.uuid4()
         cases = [
-            ("je says pas", "“je says pas” is not a valid UUID.", 8),
-            (non_existent_id, f'Invalid pk "{non_existent_id}" - object does not exist.', 9),
-            (self.private_ml_class.id, f'Invalid pk "{self.private_ml_class.id}" - object does not exist.', 9),
+            ("je says pas", "“je says pas” is not a valid UUID.", 5),
+            (non_existent_id, f'Invalid pk "{non_existent_id}" - object does not exist.', 6),
+            (self.private_ml_class.id, f'Invalid pk "{self.private_ml_class.id}" - object does not exist.', 6),
         ]
         for ml_class_id, message, queries in cases:
             with self.subTest(ml_class_id=ml_class_id), self.assertNumQueries(queries):
@@ -1556,7 +1541,7 @@ class TestProcesses(FixtureAPITestCase):
         self.client.force_login(self.user)
         process = Process.objects.create(mode=ProcessMode.Workers, corpus=self.corpus, creator=self.user)
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.patch(
                 reverse("api:process-details", kwargs={"pk": process.id}),
                 {
@@ -1615,6 +1600,7 @@ class TestProcesses(FixtureAPITestCase):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": self.user_img_process.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_retry_repository_process_no_right(self):
         """
         A user that is not the creator nor admin cannot restart a process that is not linked to any corpus
@@ -1643,7 +1629,7 @@ class TestProcesses(FixtureAPITestCase):
             with self.subTest(state=state):
                 self.elts_process.tasks.all().update(state=state)
                 self.assertEqual(self.elts_process.state, state)
-                with self.assertNumQueries(11):
+                with self.assertNumQueries(6):
                     response = self.client.post(reverse("api:process-retry", kwargs={"pk": self.elts_process.id}))
                     self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
                 self.assertDictEqual(response.json(), {"__all__": [message]})
@@ -1657,7 +1643,7 @@ class TestProcesses(FixtureAPITestCase):
         self.elts_process.finished = timezone.now()
         self.elts_process.save()
 
-        with self.assertNumQueries(16):
+        with self.assertNumQueries(11):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": self.elts_process.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -1668,6 +1654,7 @@ class TestProcesses(FixtureAPITestCase):
         # Activity initialization runs again
         self.assertFalse(delay_mock.called)
 
+    @expectedFailure
     def test_retry_farm_guest(self):
         self.elts_process.run()
         self.elts_process.tasks.all().update(state=State.Error)
@@ -1700,7 +1687,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(10):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": self.elts_process.id}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -1713,7 +1700,6 @@ class TestProcesses(FixtureAPITestCase):
         process = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Workers,
-            farm=self.default_farm,
         )
         process.worker_runs.create(
             version=self.version_with_model,
@@ -1728,7 +1714,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(10):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": process.id}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -1741,7 +1727,7 @@ class TestProcesses(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.assertFalse(self.elts_process.tasks.exists())
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(10):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": self.elts_process.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -1763,7 +1749,7 @@ class TestProcesses(FixtureAPITestCase):
         self.workers_process.activity_state = ActivityState.Error
         self.workers_process.save()
 
-        with self.assertNumQueries(18):
+        with self.assertNumQueries(13):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": self.workers_process.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -1778,7 +1764,6 @@ class TestProcesses(FixtureAPITestCase):
         process = self.corpus.processes.create(
             mode=ProcessMode.Files,
             creator=self.user,
-            farm=self.default_farm,
         )
         process.worker_runs.create(version=self.version_with_model)
         process.tasks.create(state=State.Error, run=0, depth=0)
@@ -1787,7 +1772,7 @@ class TestProcesses(FixtureAPITestCase):
 
         with (
             self.settings(IMPORTS_WORKER_VERSION=str(self.version_with_model.id)),
-            self.assertNumQueries(19),
+            self.assertNumQueries(14),
         ):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": process.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -1808,7 +1793,6 @@ class TestProcesses(FixtureAPITestCase):
             folder_type=self.volume_type,
             element_type=self.page_type,
             creator=self.user,
-            farm=self.default_farm,
         )
         process.worker_runs.create(version=self.version_with_model)
         process.tasks.create(state=State.Error, run=0, depth=0)
@@ -1824,7 +1808,7 @@ class TestProcesses(FixtureAPITestCase):
                 IMPORTS_WORKER_VERSION=str(self.version_with_model.id),
                 INGEST_IMAGESERVER_ID=str(ImageServer.objects.get(url="http://server").id),
             ),
-            self.assertNumQueries(20),
+            self.assertNumQueries(15),
         ):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": process.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -1842,7 +1826,6 @@ class TestProcesses(FixtureAPITestCase):
         process = self.corpus.processes.create(
             mode=ProcessMode.IIIF,
             creator=self.user,
-            farm=self.default_farm,
         )
         process.worker_runs.create(version=self.version_with_model)
         process.tasks.create(state=State.Error, run=0, depth=0)
@@ -1851,7 +1834,7 @@ class TestProcesses(FixtureAPITestCase):
 
         with (
             self.settings(IMPORTS_WORKER_VERSION=str(self.version_with_model.id)),
-            self.assertNumQueries(19),
+            self.assertNumQueries(14),
         ):
             response = self.client.post(reverse("api:process-retry", kwargs={"pk": process.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -1878,7 +1861,7 @@ class TestProcesses(FixtureAPITestCase):
 
         with (
             self.settings(IMPORTS_WORKER_VERSION=str(self.version_with_model.id)),
-            self.assertNumQueries(30)
+            self.assertNumQueries(25),
         ):
             response = self.client.post(reverse("api:files-process"), {
                 "files": [str(self.img_df.id)],
@@ -1893,7 +1876,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertListEqual(list(process.files.all()), [self.img_df])
         self.assertEqual(process.folder_type.slug, "volume")
         self.assertEqual(process.element_type.slug, "page")
-        self.assertEqual(process.farm, self.default_farm)
+        self.assertEqual(process.farm, None)
         self.assertEqual(self.version_with_model.worker_runs.count(), 1)
         import_task = process.tasks.get(slug="import_files")
         self.assertEqual(
@@ -1932,7 +1915,7 @@ class TestProcesses(FixtureAPITestCase):
             for content_type in settings.ARCHIVE_MIME_TYPES
         )
 
-        with self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)), self.assertNumQueries(42):
+        with self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)), self.assertNumQueries(37):
             response = self.client.post(reverse("api:files-process"), {
                 "files": [
                     str(self.pdf_df.id),
@@ -1965,7 +1948,7 @@ class TestProcesses(FixtureAPITestCase):
     def test_from_files_iiif(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(30), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
+        with self.assertNumQueries(25), self.settings(IMPORTS_WORKER_VERSION=str(self.import_worker_version.id)):
             response = self.client.post(reverse("api:files-process"), {
                 "files": [str(self.iiif_df.id)],
                 "mode": "iiif",
@@ -2074,7 +2057,7 @@ class TestProcesses(FixtureAPITestCase):
     def test_from_files_files_wrong_type(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.post(reverse("api:files-process"), {
                 "files": [str(self.iiif_df.id)],
                 "folder_type": "volume",
@@ -2117,23 +2100,25 @@ class TestProcesses(FixtureAPITestCase):
     def test_from_files_farm(self):
         self.client.force_login(self.user)
         self.assertEqual(self.version_with_model.worker_runs.count(), 0)
+        farm = Farm.objects.get(name="Wheat farm")
 
         with (
             self.settings(IMPORTS_WORKER_VERSION=str(self.version_with_model.id)),
-            self.assertNumQueries(30)
+            self.assertNumQueries(26),
         ):
             response = self.client.post(reverse("api:files-process"), {
                 "files": [str(self.img_df.id)],
                 "folder_type": "volume",
                 "element_type": "page",
-                "farm_id": str(self.other_farm.id),
+                "farm_id": str(farm.id),
             }, format="json")
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
         data = response.json()
         process = Process.objects.get(id=data["id"])
-        self.assertEqual(process.farm, self.other_farm)
+        self.assertEqual(process.farm, farm)
 
+    @expectedFailure
     def test_from_files_farm_guest(self):
         self.client.force_login(self.user)
         self.assertEqual(self.version_with_model.worker_runs.count(), 0)
@@ -2155,26 +2140,6 @@ class TestProcesses(FixtureAPITestCase):
             "farm_id": ["You do not have access to this farm."],
         })
 
-    def test_from_files_default_farm_guest(self):
-        self.client.force_login(self.user)
-        self.assertEqual(self.version_with_model.worker_runs.count(), 0)
-        self.default_farm.memberships.filter(user=self.user).delete()
-
-        with (
-            self.settings(IMPORTS_WORKER_VERSION=str(self.version_with_model.id)),
-            self.assertNumQueries(8)
-        ):
-            response = self.client.post(reverse("api:files-process"), {
-                "files": [str(self.img_df.id)],
-                "folder_type": "volume",
-                "element_type": "page",
-            }, format="json")
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
-        self.assertEqual(response.json(), {
-            "farm_id": ["You do not have access to this farm."],
-        })
-
     def test_start_process_requires_login(self):
         response = self.client.post(
             reverse("api:process-start", kwargs={"pk": str(self.user_img_process.id)})
@@ -2207,7 +2172,6 @@ class TestProcesses(FixtureAPITestCase):
         process2 = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Workers,
-            farm=self.default_farm,
         )
         process2.run()
         self.assertTrue(process2.tasks.exists())
@@ -2227,7 +2191,6 @@ class TestProcesses(FixtureAPITestCase):
         process2 = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Workers,
-            farm=self.default_farm,
         )
         process2.worker_runs.create(version=self.version_with_model, parents=[], configuration=None)
         process2.save()
@@ -2247,7 +2210,6 @@ class TestProcesses(FixtureAPITestCase):
         process2 = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Workers,
-            farm=self.default_farm,
         )
         run = process2.worker_runs.create(version=self.version_with_model, parents=[], configuration=None)
         run.model_version = self.model_version_1
@@ -2255,7 +2217,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process2.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2266,11 +2228,10 @@ class TestProcesses(FixtureAPITestCase):
         process2 = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Workers,
-            farm=self.default_farm,
         )
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2285,7 +2246,6 @@ class TestProcesses(FixtureAPITestCase):
         process2 = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Workers,
-            farm=self.default_farm,
         )
         process2.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
         self.recognizer.state = WorkerVersionState.Error
@@ -2293,7 +2253,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process2.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2311,7 +2271,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process2.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2329,7 +2289,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process2.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(9):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2378,7 +2338,7 @@ class TestProcesses(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2402,7 +2362,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2411,8 +2371,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(response.json()["id"], str(process2.id))
         process2.refresh_from_db()
         self.assertEqual(process2.state, State.Unscheduled)
-        # Ensure default parameters are used
-        self.assertEqual(process2.farm, self.default_farm)
+        self.assertEqual(process2.farm, None)
         self.assertEqual(process2.tasks.count(), 2)
         task1, task2 = process2.tasks.order_by("slug")
         self.assertEqual(task1.slug, "initialisation")
@@ -2426,7 +2385,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2443,7 +2402,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2460,7 +2419,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)}),
                 {
@@ -2486,7 +2445,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(18):
+        with self.assertNumQueries(12):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2496,7 +2455,7 @@ class TestProcesses(FixtureAPITestCase):
         process2.refresh_from_db()
         self.assertEqual(process2.state, State.Unscheduled)
         # Ensure default parameters are used
-        self.assertEqual(process2.farm, self.default_farm)
+        self.assertEqual(process2.farm, None)
         self.assertEqual(process2.tasks.count(), 1)
         task = process2.tasks.get()
         self.assertEqual(task.slug, run.task_slug)
@@ -2518,7 +2477,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)})
             )
@@ -2537,19 +2496,21 @@ class TestProcesses(FixtureAPITestCase):
         """
         workers_process = self.corpus.processes.create(creator=self.user, mode=ProcessMode.Workers)
         workers_process.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
-        self.client.force_login(self.user)
+        farm = Farm.objects.get(name="Wheat farm")
 
-        with self.assertNumQueries(20):
+        self.client.force_login(self.user)
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(workers_process.id)}),
-                {"farm": str(self.other_farm.id)}
+                {"farm": str(farm.id)}
             )
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
         workers_process.refresh_from_db()
         self.assertEqual(workers_process.state, State.Unscheduled)
-        self.assertEqual(workers_process.farm_id, self.other_farm.id)
+        self.assertEqual(workers_process.farm_id, farm.id)
 
+    @expectedFailure
     def test_start_process_default_farm_guest(self):
         workers_process = self.corpus.processes.create(creator=self.user, mode=ProcessMode.Workers)
         workers_process.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
@@ -2569,6 +2530,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(workers_process.state, State.Unscheduled)
         self.assertIsNone(workers_process.farm)
 
+    @expectedFailure
     def test_start_process_farm_guest(self):
         workers_process = self.corpus.processes.create(creator=self.user, mode=ProcessMode.Workers)
         workers_process.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
@@ -2597,7 +2559,7 @@ class TestProcesses(FixtureAPITestCase):
         self.client.force_login(self.user)
         wrong_farm_id = uuid.uuid4()
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(workers_process.id)}),
                 {"farm": str(wrong_farm_id)}
@@ -2616,7 +2578,7 @@ class TestProcesses(FixtureAPITestCase):
             ({"thumbnails": "gloubiboulga"}, {"thumbnails": ["Must be a valid boolean."]})
         ]
         for (params, check) in wrong_params_checks:
-            with self.subTest(**params), self.assertNumQueries(11):
+            with self.subTest(**params), self.assertNumQueries(5):
                 response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}), params)
                 self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
                 self.assertDictEqual(response.json(), check)
@@ -2630,7 +2592,7 @@ class TestProcesses(FixtureAPITestCase):
         process.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"chunks": 43},
@@ -2703,7 +2665,7 @@ class TestProcesses(FixtureAPITestCase):
             element_type=self.corpus.types.get(slug="page")
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"use_cache": "true", "worker_activity": "true", "use_gpu": "true"}
@@ -2735,7 +2697,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(21):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"use_cache": "true", "worker_activity": "true", "use_gpu": "true"}
@@ -2771,7 +2733,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertNotEqual(run_1.task_slug, run_2.task_slug)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(14):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -2793,7 +2755,6 @@ class TestProcesses(FixtureAPITestCase):
         process = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Files,
-            farm=self.default_farm,
         )
         process.files.create(name="A PDF file", size=10e6, content_type="pdf", corpus=self.corpus)
         token_mock.return_value = "12345"
@@ -2826,7 +2787,6 @@ class TestProcesses(FixtureAPITestCase):
         process = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Workers,
-            farm=self.default_farm,
         )
         token_mock.side_effect = [b"12345", b"78945"]
         run = process.worker_runs.create(version=self.version_with_model, parents=[], configuration=None)
@@ -2873,7 +2833,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(process.worker_runs.count(), 2)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.delete(reverse("api:clear-process", kwargs={"pk": str(process.id)}))
         self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
         process.refresh_from_db()
@@ -2936,7 +2896,6 @@ class TestProcesses(FixtureAPITestCase):
             creator=self.user,
             mode=ProcessMode.Workers,
             element_type=self.corpus.types.get(slug="page"),
-            farm=self.default_farm,
         )
         process.worker_runs.create(version=self.dla, parents=[], configuration=None)
         process.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
@@ -2946,7 +2905,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(process.state, State.Running)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:clear-process", kwargs={"pk": str(process.id)}))
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"__all__": ["A process can only be cleared before getting started."]})
@@ -2959,7 +2918,6 @@ class TestProcesses(FixtureAPITestCase):
             creator=self.user,
             mode=ProcessMode.Workers,
             element_type=self.corpus.types.get(slug="page"),
-            farm=self.default_farm,
         )
         process.worker_runs.create(version=self.dla, parents=[], configuration=None)
         process.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
@@ -2968,17 +2926,17 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(process.state, State.Unscheduled)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse("api:clear-process", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"__all__": ["A process can only be cleared before getting started."]})
 
+    @expectedFailure
     def test_clear_process_requires_permissions(self):
         process = self.corpus.processes.create(
             creator=self.user,
             mode=ProcessMode.Workers,
             element_type=self.corpus.types.get(slug="page"),
-            farm=self.default_farm,
         )
         process.worker_runs.create(version=self.dla, parents=[], configuration=None)
         process.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
@@ -2990,7 +2948,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.corpus.memberships.create(user=user2, level=Role.Contributor.value)
         self.client.force_login(user2)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(8):
             response = self.client.delete(reverse("api:clear-process", kwargs={"pk": str(process.id)}))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have a sufficient access level to this process."})
@@ -3021,6 +2979,7 @@ class TestProcesses(FixtureAPITestCase):
             response = self.client.post(reverse("api:process-select-failures", kwargs={"pk": str(uuid.uuid4())}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+    @expectedFailure
     def test_select_failed_elts_requires_corpus_read_access(self):
         self.client.force_login(self.user)
         self.elts_process.corpus.memberships.filter(user=self.user).delete()
@@ -3042,7 +3001,7 @@ class TestProcesses(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.elts_process.activity_state = ActivityState.Disabled
         self.elts_process.save()
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:process-select-failures", kwargs={"pk": str(self.elts_process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(
@@ -3091,7 +3050,7 @@ class TestProcesses(FixtureAPITestCase):
             state=WorkerActivityState.Processed,
         )
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:process-select-failures", kwargs={"pk": str(self.elts_process.id)})
             )
@@ -3127,7 +3086,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.assertCountEqual(list(self.user.selections.values_list("element__name", flat=True)), [])
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-select-failures", kwargs={"pk": str(self.elts_process.id)})
             )
diff --git a/arkindex/process/tests/test_repos.py b/arkindex/process/tests/test_repos.py
index d5c32e7a16..607efa3d20 100644
--- a/arkindex/process/tests/test_repos.py
+++ b/arkindex/process/tests/test_repos.py
@@ -54,19 +54,11 @@ class TestRepositories(FixtureTestCase):
             response = self.client.get(reverse("api:repository-list"))
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_list_repository_external_user(self):
-        self.client.force_login(self.user)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:repository-list"))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        data = response.json()
-        self.assertCountEqual(data["results"], [])
-
     def test_list_repository_user_readable(self):
         self.repo.memberships.create(user=self.user, level=Role.Admin.value)
         self.repo_2.memberships.create(user=self.user, level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(6):
             response = self.client.get(reverse("api:repository-list"))
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
@@ -99,14 +91,6 @@ class TestRepositories(FixtureTestCase):
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertEqual(response.json(), {"detail": "Not found."})
 
-    def test_repository_retrieve_no_right(self):
-        self.client.force_login(self.user)
-        self.assertFalse(self.user.is_admin)
-        response = self.client.get(
-            reverse("api:repository-retrieve", kwargs={"pk": str(self.repo.id)})
-        )
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
     def test_repository_retrieve(self):
         """
         An user with an access right on a repository is able to retrieve it without its clone URL
@@ -138,7 +122,7 @@ class TestRepositories(FixtureTestCase):
         task = process.tasks.get()
         self.repo_2.memberships.create(user=self.user, level=Role.Guest.value)
 
-        with self.assertNumQueries(4):
+        with self.assertNumQueries(3):
             response = self.client.get(
                 reverse("api:repository-retrieve", kwargs={"pk": str(self.repo_2.id)}),
                 HTTP_AUTHORIZATION=f"Ponos {task.token}",
@@ -165,7 +149,7 @@ class TestRepositories(FixtureTestCase):
         task = process.tasks.get()
         self.repo_2.memberships.create(user=self.user, level=Role.Guest.value)
 
-        with self.assertNumQueries(4):
+        with self.assertNumQueries(3):
             response = self.client.get(
                 reverse("api:repository-retrieve", kwargs={"pk": str(self.repo_2.id)}),
                 HTTP_AUTHORIZATION=f"Ponos {task.token}",
@@ -182,7 +166,7 @@ class TestRepositories(FixtureTestCase):
     def test_repository_retrieve_disabled_repo(self):
         self.repo_2.memberships.create(user=self.user, level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:repository-retrieve", kwargs={"pk": str(self.repo_2.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         data = response.json()
@@ -230,17 +214,6 @@ class TestRepositories(FixtureTestCase):
         self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertEqual(response.json(), {"detail": "Not found."})
 
-    def test_revision_retrieve_read_right(self):
-        """
-        A user with no right or a read right is not allowed to retrieve a revision
-        """
-        self.repo.memberships.create(user=self.user, level=Role.Guest.value)
-        self.client.force_login(self.user)
-        response = self.client.get(
-            reverse("api:revision-retrieve", kwargs={"pk": str(self.rev.id)})
-        )
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
     def test_revision_retrieve_contributor_right(self):
         """
         A user with a contributor access to the revision repo is allowed to retrieve information
diff --git a/arkindex/process/tests/test_signals.py b/arkindex/process/tests/test_signals.py
index 4fbbd69021..3719074bad 100644
--- a/arkindex/process/tests/test_signals.py
+++ b/arkindex/process/tests/test_signals.py
@@ -1,28 +1,10 @@
-import hashlib
-import uuid
-from datetime import datetime, timezone
-from unittest.mock import patch
 
-from django.urls import reverse
-from rest_framework import status
 from rest_framework.exceptions import ValidationError
 
-from arkindex.ponos.authentication import AgentUser
-from arkindex.ponos.models import Farm, State
-from arkindex.process.models import (
-    ActivityState,
-    ProcessMode,
-    Repository,
-    Worker,
-    WorkerActivityState,
-    WorkerRun,
-    WorkerType,
-    WorkerVersion,
-)
+from arkindex.ponos.models import Farm
+from arkindex.process.models import ProcessMode, Repository, Worker, WorkerRun, WorkerType, WorkerVersion
 from arkindex.process.signals import _list_ancestors
 from arkindex.project.tests import FixtureAPITestCase
-from arkindex.project.tools import build_public_key
-from arkindex.training.models import Model
 
 
 class TestSignals(FixtureAPITestCase):
@@ -72,19 +54,6 @@ class TestSignals(FixtureAPITestCase):
             mode=ProcessMode.Workers,
             farm=cls.farm,
         )
-        pubkey = build_public_key()
-        cls.agent = AgentUser.objects.create(
-            id=uuid.UUID(hashlib.md5(pubkey.encode("utf-8")).hexdigest()),
-            farm=cls.farm,
-            hostname="ghostname",
-            cpu_cores=2,
-            cpu_frequency=1e9,
-            public_key=pubkey,
-            ram_total=2e9,
-            last_ping=datetime.now(),
-            cpu_load=.1,
-            ram_load=.1e9,
-        )
 
     def test_worker_run_check_parents_recursive(self):
         run_2 = self.process_1.worker_runs.create(
@@ -292,156 +261,3 @@ class TestSignals(FixtureAPITestCase):
         )
 
         self.assertIsNotNone(run.summary)
-
-    @patch("arkindex.ponos.serializers.TaskSerializer.get_logs")
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    def test_task_failure_updates_activities(self, notify_mock, get_logs_mock):
-        """
-        A Ponos task failure should update `Started` worker activities to `Error`.
-        """
-        get_logs_mock.return_value = None
-
-        self.process_2.activity_state = ActivityState.Ready
-        self.process_2.save()
-        self.process_2.run()
-        task = self.process_2.tasks.first()
-
-        # Create one activity per WorkerActivityState on random elements
-        element1, element2, element3, element4 = self.corpus.elements.all()[:4]
-        queued_activity = self.process_2.activities.create(
-            worker_version=self.version_1,
-            element=element1,
-            state=WorkerActivityState.Queued,
-        )
-        processed_activity = self.process_2.activities.create(
-            worker_version=self.version_1,
-            element=element3,
-            state=WorkerActivityState.Processed,
-        )
-        error_activity = self.process_2.activities.create(
-            worker_version=self.version_1,
-            element=element4,
-            state=WorkerActivityState.Error,
-        )
-
-        # Some state changes are only allowed when a task is in a specific state,
-        # so our cases list has an initial task state, a state to update the task to,
-        # and an expected activity state for `started_activity`
-        cases = [
-            (State.Pending, State.Running, WorkerActivityState.Started),
-            (State.Running, State.Error, WorkerActivityState.Error),
-            (State.Running, State.Failed, WorkerActivityState.Error),
-            (State.Running, State.Completed, WorkerActivityState.Started),
-            (State.Stopping, State.Stopped, WorkerActivityState.Error),
-        ]
-
-        started_activity = None
-        for old_task_state, new_task_state, expected_activity_state in cases:
-            # Reset activity state between each case
-            if started_activity:
-                started_activity.delete()
-            started_activity = self.process_2.activities.create(
-                worker_version=self.version_1,
-                element=element2,
-                state=WorkerActivityState.Started,
-                started=datetime.now(timezone.utc),
-            )
-            task.state = old_task_state
-            task.agent = self.agent
-            task.save()
-            with self.subTest(old_state=old_task_state, new_state=new_task_state):
-                resp = self.client.patch(
-                    reverse("api:task-details", kwargs={"pk": task.id}),
-                    data={"state": new_task_state.value},
-                    HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-                )
-                self.assertEqual(resp.status_code, status.HTTP_200_OK)
-                task.refresh_from_db()
-                self.assertEqual(task.state, new_task_state)
-
-                queued_activity.refresh_from_db()
-                started_activity.refresh_from_db()
-                processed_activity.refresh_from_db()
-                error_activity.refresh_from_db()
-
-                self.assertEqual(queued_activity.state, WorkerActivityState.Queued)
-                self.assertEqual(processed_activity.state, WorkerActivityState.Processed)
-                self.assertEqual(error_activity.state, WorkerActivityState.Error)
-                self.assertEqual(started_activity.state, expected_activity_state)
-
-    @patch("arkindex.ponos.serializers.TaskSerializer.get_logs")
-    @patch("arkindex.ponos.tasks.notify_process_completion.delay")
-    def test_task_failure_filters_activities_worker_run(self, notify_mock, get_logs_mock):
-        """
-        A Ponos task failure only updates worker activity with the same worker version,
-        model version and worker configuration.
-        """
-        get_logs_mock.return_value = None
-        model = Model.objects.create(name="Generic model", public=False)
-        model_version = model.versions.create(hash="b" * 32, archive_hash="a" * 32, size=8)
-        worker_configuration = self.worker_1.configurations.create(name="conf")
-        self.run_1.configuration = worker_configuration
-        self.run_1.save()
-
-        self.process_2.activity_state = ActivityState.Ready
-        self.process_2.save()
-        self.process_2.run()
-        task = self.process_2.tasks.first()
-        task.state = State.Running
-        task.agent = self.agent
-        task.save()
-
-        # Create one activity per WorkerActivityState on random elements
-        element = self.corpus.elements.first()
-        activity_1 = self.process_2.activities.create(
-            worker_version=self.version_1,
-            configuration=worker_configuration,
-            model_version=None,
-            element=element,
-            started=datetime.now(),
-        )
-        activity_2 = self.process_2.activities.create(
-            worker_version=self.version_1,
-            configuration=None,
-            model_version=model_version,
-            element=element,
-            started=datetime.now(),
-        )
-        activity_3 = self.process_2.activities.create(
-            worker_version=self.version_1,
-            configuration=worker_configuration,
-            model_version=model_version,
-            element=element,
-            started=datetime.now(),
-        )
-
-        cases = [
-            (None, None, None, [activity_1, activity_2, activity_3]),
-            (self.run_1, model_version, None, [activity_2]),
-            (self.run_1, None, worker_configuration, [activity_1]),
-            (self.run_1, model_version, worker_configuration, [activity_3]),
-        ]
-        for worker_run, model_version, worker_configuration, expected in cases:
-            self.process_2.activities.update(state=WorkerActivityState.Started)
-            if worker_run:
-                worker_run.model_version = model_version
-                worker_run.configuration = worker_configuration
-                worker_run.save()
-                task.worker_run = worker_run
-                task.save()
-
-            with self.subTest(
-                worker_run=worker_run,
-                model_version=model_version,
-                worker_configuration=worker_configuration
-            ):
-                resp = self.client.patch(
-                    reverse("api:task-details", kwargs={"pk": task.id}),
-                    data={"state": State.Error.value},
-                    HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
-                )
-                self.assertEqual(resp.status_code, status.HTTP_200_OK)
-                for activity in self.process_2.activities.filter(id__in=[e.id for e in expected]):
-                    self.assertEqual(activity.state, WorkerActivityState.Error)
-                for activity in self.process_2.activities.exclude(id__in=[e.id for e in expected]):
-                    self.assertEqual(activity.state, WorkerActivityState.Started)
diff --git a/arkindex/process/tests/test_templates.py b/arkindex/process/tests/test_templates.py
index 753421c531..49f8b82e94 100644
--- a/arkindex/process/tests/test_templates.py
+++ b/arkindex/process/tests/test_templates.py
@@ -1,5 +1,6 @@
 import json
 from datetime import datetime, timezone
+from unittest import expectedFailure
 
 from rest_framework import status
 from rest_framework.reverse import reverse
@@ -88,7 +89,7 @@ class TestTemplates(FixtureAPITestCase):
 
     def test_create(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(9):
             response = self.client.post(
                 reverse(
                     "api:create-process-template", kwargs={"pk": str(self.process_template.id)}
@@ -138,6 +139,7 @@ class TestTemplates(FixtureAPITestCase):
         )
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_create_requires_contributor_access_rights_process(self):
         new_user = User.objects.create(email="new@test.fr", verified_email=True)
         self.worker_1.memberships.create(user=new_user, level=Role.Contributor.value)
@@ -155,6 +157,7 @@ class TestTemplates(FixtureAPITestCase):
             {"detail": "You do not have a contributor access to this process."},
         )
 
+    @expectedFailure
     def test_create_requires_access_rights_all_workers(self):
         new_user = User.objects.create(email="new@test.fr", verified_email=True)
         self.private_corpus.memberships.create(user=new_user, level=Role.Contributor.value)
@@ -177,7 +180,7 @@ class TestTemplates(FixtureAPITestCase):
                 self.process.corpus = None if mode == ProcessMode.Repository else self.corpus
                 self.process.save()
 
-                with self.assertNumQueries(11):
+                with self.assertNumQueries(5):
                     response = self.client.post(
                         reverse("api:create-process-template", kwargs={"pk": str(self.template.id)}),
                         data=json.dumps({"process_id": str(self.process.id)}),
@@ -193,7 +196,7 @@ class TestTemplates(FixtureAPITestCase):
         self.client.force_login(self.user)
         local_process = self.user.processes.get(mode=ProcessMode.Local)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:create-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(local_process.id)}),
@@ -205,6 +208,7 @@ class TestTemplates(FixtureAPITestCase):
         local_process.refresh_from_db()
         self.assertEqual(local_process.template, None)
 
+    @expectedFailure
     def test_apply_requires_contributor_rights_on_template(self):
         """Raise 403 if the user does not have rights on template
         """
@@ -227,6 +231,7 @@ class TestTemplates(FixtureAPITestCase):
             {"detail": "You do not have a contributor access to this process."},
         )
 
+    @expectedFailure
     def test_apply_requires_contributor_rights_on_process(self):
         """Raise 403 if the user does not have rights on the target process
         """
@@ -249,6 +254,7 @@ class TestTemplates(FixtureAPITestCase):
             {"detail": "You do not have a contributor access to this process."},
         )
 
+    @expectedFailure
     def test_apply_requires_access_rights_all_workers(self):
         """Raise 403 if the user does not have rights on all workers concerned
         """
@@ -303,7 +309,7 @@ class TestTemplates(FixtureAPITestCase):
     def test_apply(self):
         self.assertIsNotNone(self.version_2.docker_image_id)
         self.client.force_login(self.user)
-        with self.assertNumQueries(18):
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(self.process.id)}),
@@ -336,7 +342,7 @@ class TestTemplates(FixtureAPITestCase):
         self.version_2.save()
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(18):
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(self.process.id)}),
@@ -356,7 +362,7 @@ class TestTemplates(FixtureAPITestCase):
             parents=[],
         )
         # Apply a template that has two other worker runs
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(process.id)}),
@@ -384,7 +390,7 @@ class TestTemplates(FixtureAPITestCase):
         self.version_2.state = WorkerVersionState.Error
         self.version_2.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(self.process.id)}),
@@ -399,7 +405,7 @@ class TestTemplates(FixtureAPITestCase):
         self.worker_2.archived = datetime.now(timezone.utc)
         self.worker_2.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(self.process.id)}),
@@ -415,7 +421,7 @@ class TestTemplates(FixtureAPITestCase):
         self.model_version.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(self.process.id)}),
@@ -433,7 +439,7 @@ class TestTemplates(FixtureAPITestCase):
         self.model.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(self.process.id)}),
@@ -452,7 +458,7 @@ class TestTemplates(FixtureAPITestCase):
                 self.process.mode = mode
                 self.process.save()
 
-                with self.assertNumQueries(14):
+                with self.assertNumQueries(7):
                     response = self.client.post(
                         reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                         data=json.dumps({"process_id": str(self.process.id)}),
@@ -470,7 +476,7 @@ class TestTemplates(FixtureAPITestCase):
         self.client.force_login(self.user)
         local_process = self.user.processes.get(mode=ProcessMode.Local)
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(local_process.id)}),
@@ -488,7 +494,7 @@ class TestTemplates(FixtureAPITestCase):
         self.client.force_login(self.user)
         process = Process.objects.filter(mode=ProcessMode.Repository).first()
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:apply-process-template", kwargs={"pk": str(self.template.id)}),
                 data=json.dumps({"process_id": str(process.id)}),
@@ -504,7 +510,7 @@ class TestTemplates(FixtureAPITestCase):
 
     def test_list_ignores_configuration_filter(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:process-list"),
                 data={"mode": "template", "with_tasks": True},
diff --git a/arkindex/process/tests/test_user_workerruns.py b/arkindex/process/tests/test_user_workerruns.py
index f42698d2de..69c05a1d5b 100644
--- a/arkindex/process/tests/test_user_workerruns.py
+++ b/arkindex/process/tests/test_user_workerruns.py
@@ -1,5 +1,5 @@
 from datetime import datetime
-from unittest.mock import patch
+from unittest.mock import call, patch
 
 from django.urls import reverse
 from django.utils import timezone
@@ -15,7 +15,7 @@ from arkindex.process.models import (
     WorkerVersionState,
 )
 from arkindex.project.tests import FixtureAPITestCase
-from arkindex.training.models import Model, ModelVersionState
+from arkindex.training.models import Model, ModelVersion, ModelVersionState
 from arkindex.users.models import Right, Role, User
 
 
@@ -203,7 +203,7 @@ class TestUserWorkerRuns(FixtureAPITestCase):
 
     def test_create_user_run(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.other_version.id)}
@@ -257,7 +257,7 @@ class TestUserWorkerRuns(FixtureAPITestCase):
         Right.objects.create(user=test_user, content_object=self.other_version.worker, level=Role.Contributor.value)
         self.client.force_login(test_user)
         self.assertFalse(Process.objects.filter(mode=ProcessMode.Local, creator=test_user).exists())
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.other_version.id)}
@@ -278,7 +278,7 @@ class TestUserWorkerRuns(FixtureAPITestCase):
     def test_create_user_run_wv_has_revision(self):
         Right.objects.create(user=self.user, content_object=self.version_1.worker, level=Role.Contributor.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.version_1.id)}
@@ -290,7 +290,7 @@ class TestUserWorkerRuns(FixtureAPITestCase):
         Right.objects.create(user=self.user, content_object=self.version_1.worker, level=Role.Contributor.value)
         test_version = self.version_1.worker.versions.create(configuration={}, version=7)
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(test_version.id)}
@@ -303,7 +303,7 @@ class TestUserWorkerRuns(FixtureAPITestCase):
         self.custom_version.worker.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.custom_version.id)}
@@ -312,21 +312,25 @@ class TestUserWorkerRuns(FixtureAPITestCase):
 
         self.assertEqual(response.json(), {"worker_version_id": ["The worker used to create a local worker run must not be archived."]})
 
-    def test_create_user_run_worker_not_executable(self):
-        test_user = User.objects.get(email="user2@user.fr")
-        Right.objects.create(user=test_user, content_object=self.other_version.worker, level=Role.Guest.value)
-        self.client.force_login(test_user)
-        with self.assertNumQueries(6):
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Guest.value)
+    def test_create_user_run_worker_not_executable(self, get_max_level_mock):
+        self.client.force_login(self.user)
+
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.other_version.id)}
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertEqual(response.json(), {"worker_version_id": ["You do not have contributor access to this worker."]})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.other_version.worker))
+
     def test_create_user_run_mv_doesnt_exist(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.other_version.id), "model_version_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
@@ -336,7 +340,7 @@ class TestUserWorkerRuns(FixtureAPITestCase):
 
     def test_create_user_run_wc_doesnt_exist(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.other_version.id), "configuration_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}
@@ -344,23 +348,31 @@ class TestUserWorkerRuns(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(response.json(), {"configuration_id": ['Invalid pk "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - object does not exist.']})
 
-    def test_create_user_run_model_no_access(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=ModelVersion.objects.none())
+    def test_create_user_run_model_no_access(self, filter_rights_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.other_version.id), "model_version_id": str(self.model_version.id)}
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertEqual(response.json(), {"model_version_id": ["You do not have guest access to this model."]})
 
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Model, Role.Guest.value),
+            call(self.user, Model, Role.Contributor.value),
+        ])
+
     def test_create_user_run_model_archived(self):
         self.model.archived = datetime.now(timezone.utc)
         self.model.save()
         self.model.memberships.create(user=self.user, level=Role.Contributor.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={"worker_version_id": str(self.other_version.id), "model_version_id": str(self.model_version.id)}
@@ -378,7 +390,7 @@ class TestUserWorkerRuns(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={
@@ -445,7 +457,7 @@ class TestUserWorkerRuns(FixtureAPITestCase):
 
     def test_create_user_run_duplicate(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:user-worker-run-create"),
                 data={
diff --git a/arkindex/process/tests/test_utils.py b/arkindex/process/tests/test_utils.py
deleted file mode 100644
index 6c6651e6ac..0000000000
--- a/arkindex/process/tests/test_utils.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import uuid
-
-from django.test import TestCase, override_settings
-
-from arkindex.ponos.models import Farm
-from arkindex.process.utils import get_default_farm
-
-DEFAULT_FARM_ID = str(uuid.uuid4())
-
-
-class TestProcessUtils(TestCase):
-    """
-    Test utilities functions for process module
-    """
-
-    @classmethod
-    def setUpTestData(cls):
-        super().setUpTestData()
-        cls.corn_farm = Farm.objects.create(id=DEFAULT_FARM_ID, name="Corn farm")
-        cls.barley_farm = Farm.objects.create(name="Barley farm")
-
-    def setUp(self):
-        super().setUp()
-        # Force clean the module default farm cached global variable
-        from arkindex.process import utils
-        setattr(utils, "__default_farm", None)
-
-    @override_settings(PONOS_DEFAULT_FARM=DEFAULT_FARM_ID)
-    def test_config_ponos_farm_setting(self):
-        """
-        Default farm may be defined in settings
-        """
-        with self.assertNumQueries(1):
-            self.assertEqual(str(get_default_farm()), str(self.corn_farm))
-
-    def test_default_ponos_farm(self):
-        """
-        In case no default farm is defined, a random farm is selected as default.
-        Subsequent calls does not hit the database
-        """
-        self.assertEqual(Farm.objects.count(), 2)
-
-        with self.assertNumQueries(1):
-            farm = get_default_farm()
-            self.assertIsInstance(farm, Farm)
-
-        # No new farm was created
-        self.assertEqual(Farm.objects.count(), 2)
-
-        with self.assertNumQueries(0):
-            self.assertEqual(get_default_farm(), farm)
-
-    def test_default_farm_no_existing_farm(self):
-        """
-        In case no farm exists, a default one is created
-        """
-        Farm.objects.all().delete()
-        self.assertEqual(Farm.objects.count(), 0)
-
-        with self.assertNumQueries(2):
-            farm = get_default_farm()
-            self.assertIsInstance(farm, Farm)
-
-        self.assertTrue(Farm.objects.filter(id=farm.id).exists())
-
-        with self.assertNumQueries(0):
-            self.assertEqual(get_default_farm(), farm)
diff --git a/arkindex/process/tests/test_worker_configurations.py b/arkindex/process/tests/test_worker_configurations.py
index e56799ee04..e1e7b9df25 100644
--- a/arkindex/process/tests/test_worker_configurations.py
+++ b/arkindex/process/tests/test_worker_configurations.py
@@ -1,3 +1,5 @@
+from unittest.mock import call, patch
+
 from django.urls import reverse
 from rest_framework import status
 
@@ -30,55 +32,32 @@ class TestWorkerConfigurations(FixtureAPITestCase):
     def test_list_requires_login(self):
         with self.assertNumQueries(0):
             response = self.client.get(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
     def test_list_requires_verified(self):
         self.user.verified_email = False
         self.user.save()
         self.client.force_login(self.user)
+
         with self.assertNumQueries(2):
             response = self.client.get(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_list_no_rights(self):
-        self.worker_1.repository.memberships.filter(user=self.user).delete()
-        self.worker_1.memberships.filter(user=self.user).delete()
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_list_no_rights(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertEqual(response.json(), {"detail": "You do not have a guest access to this worker."})
 
-    def test_list_worker_rights(self):
-        """
-        A user is able to list worker configurations if they have a guest access on the worker
-        """
-        config_1 = self.worker_2.configurations.create(name="config_1", configuration={"key": "value"})
-        config_2 = self.worker_2.configurations.create(name="config_2", configuration={"dulce": "et decorum est"})
-        self.worker_2.memberships.all().delete()
-        self.worker_2.repository.memberships.all().delete()
-        self.worker_2.memberships.create(user=self.user, level=Role.Guest.value)
-
-        self.client.force_login(self.user)
-        with self.assertNumQueries(7):
-            response = self.client.get(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_2.id)}))
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertListEqual(response.json()["results"], [{
-            "id": str(config_1.id),
-            "name": "config_1",
-            "configuration": {"key": "value"},
-            "archived": False,
-        }, {
-            "id": str(config_2.id),
-            "name": "config_2",
-            "configuration": {"dulce": "et decorum est"},
-            "archived": False,
-        }])
+        self.assertEqual(response.json(), {"detail": "You do not have a guest access to this worker."})
+        self.assertEqual(has_access_mock.call_args_list, [
+            call(self.user, self.worker_1, Role.Guest.value, skip_public=False),
+            call(self.user, self.repo, Role.Guest.value, skip_public=False),
+        ])
 
-    def test_list_repo_rights(self):
-        """
-        A user is able to list worker configurations if they have a guest access on the repository
-        """
+    def test_list(self):
         config_1 = self.worker_2.configurations.create(name="config_1", configuration={"key": "value"})
         config_2 = self.worker_2.configurations.create(name="config_2", configuration={"dulce": "et decorum est"})
         # Only non-archived configurations should be listed
@@ -87,14 +66,12 @@ class TestWorkerConfigurations(FixtureAPITestCase):
             configuration={"deprecated": "config"},
             archived=True,
         )
-        self.worker_2.memberships.all().delete()
-        self.worker_2.repository.memberships.all().delete()
-        self.worker_2.repository.memberships.create(user=self.user, level=Role.Guest.value)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_2.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertListEqual(response.json()["results"], [{
             "id": str(config_1.id),
             "name": "config_1",
@@ -119,7 +96,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}), {"archived": "true"})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -143,29 +120,23 @@ class TestWorkerConfigurations(FixtureAPITestCase):
             response = self.client.post(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_create_no_rights(self):
-        self.worker_1.repository.memberships.filter(user=self.user).delete()
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_no_rights(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertEqual(response.json(), {"detail": "You do not have contributor access to this worker."})
 
-    def test_create_contributor_ok(self):
-        self.worker_1.repository.memberships.filter(user=self.user).delete()
-        self.worker_1.memberships.create(user=self.user, level=Role.Contributor.value)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(7):
-            response = self.client.post(
-                reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}),
-                data={"name": "test_name", "configuration": {"aa": "bb"}},
-                format="json",
-            )
-            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        self.assertEqual(response.json(), {"detail": "You do not have contributor access to this worker."})
+        self.assertEqual(has_access_mock.call_args_list, [
+            call(self.user, self.worker_1, Role.Contributor.value, skip_public=False),
+            call(self.user, self.repo, Role.Contributor.value, skip_public=False),
+        ])
 
     def test_create_empty(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -179,7 +150,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.worker_2.configurations.create(name=name, configuration={"key": "value"})
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_2.id)}),
                 data={"name": name, "configuration": {"key": "value", "cahuete": "bidule"}},
@@ -197,7 +168,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         test_config = self.worker_2.configurations.create(name="config-name", configuration=config)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_2.id)}),
                 data={"name": "New configuration", "configuration": config},
@@ -217,7 +188,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         test_config = self.worker_2.configurations.create(name="a config name", configuration=config)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_2.id)}),
                 data={"name": "a config name", "configuration": config},
@@ -240,7 +211,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         test_config_2 = self.worker_2.configurations.create(name="some config", configuration=config_2)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_2.id)}),
                 data={"name": "a config name", "configuration": config_2},
@@ -266,7 +237,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.worker_2.configurations.create(name="config_1", configuration={"key": "value"})
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_2.id)}),
                 data={"name": name, "configuration": config},
@@ -287,7 +258,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.worker_2.configurations.create(name="config_1", configuration={"key": "value"})
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_2.id)}),
                 data={"name": name},
@@ -298,7 +269,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
 
     def test_create_not_json(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}),
                 data={"name": "test_config", "configuration": []}, format="json"
@@ -310,7 +281,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
 
     def test_create_null_config(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}),
                 data={"name": "test_config", "configuration": None}, format="json"
@@ -322,7 +293,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
 
     def test_create_empty_config(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:worker-configurations", kwargs={"pk": str(self.worker_1.id)}),
                 data={"name": "test_config", "configuration": {}}, format="json"
@@ -353,18 +324,24 @@ class TestWorkerConfigurations(FixtureAPITestCase):
             response = self.client.get(reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_retrieve_no_rights(self):
-        self.worker_1.repository.memberships.filter(user=self.user).delete()
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_retrieve_no_rights(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertEqual(response.json(), {"detail": "You do not have a guest access to this worker."})
+        self.assertEqual(has_access_mock.call_args_list, [
+            call(self.user, self.worker_1, Role.Guest.value, skip_public=False),
+            call(self.user, self.repo, Role.Guest.value, skip_public=False),
+        ])
 
     def test_retrieve(self):
         self.worker_config.worker.memberships.get_or_create(user=self.user, level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertDictEqual(response.json(), {
@@ -394,79 +371,33 @@ class TestWorkerConfigurations(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_update_no_rights(self):
-        """
-        Cannot update a configuration without any access rights
-        """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_update_requires_contributor(self, has_access_mock):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
                 data={"archived": True}
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertEqual(response.json(), {
-            "detail": "You do not have a contributor access to this worker.",
-        })
 
-    def test_update_guest_rights(self):
-        """
-        Cannot update a configuration when only having guest rights
-        """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
-        self.worker_1.memberships.create(user=self.user, level=Role.Guest.value)
-        self.worker_1.repository.memberships.create(user=self.user, level=Role.Guest.value)
-        self.client.force_login(self.user)
-
-        with self.assertNumQueries(6):
-            response = self.client.put(
-                reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
-                data={"archived": True}
-            )
-            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {
             "detail": "You do not have a contributor access to this worker.",
         })
+        self.assertEqual(has_access_mock.call_args_list, [
+            call(self.user, self.worker_1, Role.Contributor.value, skip_public=False),
+            call(self.user, self.repo, Role.Contributor.value, skip_public=False),
+        ])
 
-    def test_update_contributor_repository(self):
-        """
-        Can update a configuration with contributor rights on the repository
-        """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
-        self.worker_1.repository.memberships.create(user=self.user, level=Role.Contributor.value)
-        self.client.force_login(self.user)
-
-        self.assertFalse(self.worker_config.archived)
-
-        with self.assertNumQueries(8):
-            response = self.client.put(
-                reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
-                data={"archived": True, "name": "new name"}
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.worker_config.refresh_from_db()
-        self.assertEqual(self.worker_config.name, "new name")
-        self.assertDictEqual(self.worker_config.configuration, {"key": "value"})
-        self.assertTrue(self.worker_config.archived)
-
-    def test_update_contributor_worker(self):
+    def test_update(self):
         """
         Can update a configuration with contributor rights on the worker
         """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
-        self.worker_1.memberships.create(user=self.user, level=Role.Contributor.value)
         self.client.force_login(self.user)
-
         self.assertFalse(self.worker_config.archived)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
                 data={"archived": True, "name": "new name"}
@@ -478,28 +409,6 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.assertDictEqual(self.worker_config.configuration, {"key": "value"})
         self.assertTrue(self.worker_config.archived)
 
-    def test_update_admin(self):
-        """
-        Admins can update any configuration
-        """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
-        self.client.force_login(self.superuser)
-
-        self.assertFalse(self.worker_config.archived)
-
-        with self.assertNumQueries(5):
-            response = self.client.put(
-                reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
-                data={"archived": True, "name": "a new name"}
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.worker_config.refresh_from_db()
-        self.assertEqual(self.worker_config.name, "a new name")
-        self.assertDictEqual(self.worker_config.configuration, {"key": "value"})
-        self.assertTrue(self.worker_config.archived)
-
     def test_update_ignored_fields(self):
         """
         Fields that should not be editable are ignored when sent in update requests
@@ -539,7 +448,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.worker_config.save()
         self.assertTrue(self.worker_config.archived)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
                 data={
@@ -567,7 +476,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.worker_config.save()
         self.assertTrue(self.worker_config.archived)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
                 data={
@@ -590,7 +499,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.worker_1.configurations.create(name="new name", configuration={"key": "value", "key2": "value2"})
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
                 data={
@@ -625,68 +534,26 @@ class TestWorkerConfigurations(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_partial_update_no_rights(self):
-        """
-        Cannot update a configuration without any access rights
-        """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_partial_update_requires_contributor(self, has_access_mock):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
                 data={"archived": True}
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertEqual(response.json(), {
-            "detail": "You do not have a contributor access to this worker.",
-        })
-
-    def test_partial_update_guest_rights(self):
-        """
-        Cannot update a configuration when only having guest rights
-        """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
-        self.worker_1.memberships.create(user=self.user, level=Role.Guest.value)
-        self.worker_1.repository.memberships.create(user=self.user, level=Role.Guest.value)
-        self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
-            response = self.client.patch(
-                reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
-                data={"archived": True}
-            )
-            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {
             "detail": "You do not have a contributor access to this worker.",
         })
+        self.assertEqual(has_access_mock.call_args_list, [
+            call(self.user, self.worker_1, Role.Contributor.value, skip_public=False),
+            call(self.user, self.repo, Role.Contributor.value, skip_public=False),
+        ])
 
-    def test_partial_update_contributor_repository(self):
-        """
-        Can partial update a configuration with contributor rights on the repository
-        """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
-        self.worker_1.repository.memberships.create(user=self.user, level=Role.Contributor.value)
-        self.client.force_login(self.user)
-
-        self.assertFalse(self.worker_config.archived)
-
-        with self.assertNumQueries(7):
-            response = self.client.patch(
-                reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
-                data={"archived": True}
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.worker_config.refresh_from_db()
-        self.assertEqual(self.worker_config.name, "config time")
-        self.assertDictEqual(self.worker_config.configuration, {"key": "value"})
-        self.assertTrue(self.worker_config.archived)
-
-    def test_partial_update_contributor_worker(self):
+    def test_partial_update(self):
         """
         Can partial update a configuration with contributor rights on the worker
         """
@@ -697,28 +564,6 @@ class TestWorkerConfigurations(FixtureAPITestCase):
 
         self.assertFalse(self.worker_config.archived)
 
-        with self.assertNumQueries(6):
-            response = self.client.patch(
-                reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
-                data={"archived": True}
-            )
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.worker_config.refresh_from_db()
-        self.assertEqual(self.worker_config.name, "config time")
-        self.assertDictEqual(self.worker_config.configuration, {"key": "value"})
-        self.assertTrue(self.worker_config.archived)
-
-    def test_partial_update_admin(self):
-        """
-        Admins can partial update any configuration
-        """
-        self.worker_1.memberships.all().delete()
-        self.worker_1.repository.memberships.all().delete()
-        self.client.force_login(self.superuser)
-
-        self.assertFalse(self.worker_config.archived)
-
         with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
@@ -770,7 +615,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.worker_config.save()
         self.assertTrue(self.worker_config.archived)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
                 data={
@@ -796,7 +641,7 @@ class TestWorkerConfigurations(FixtureAPITestCase):
         self.worker_1.configurations.create(name="new name", configuration={"key": "value", "key2": "value2"})
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:configuration-retrieve", kwargs={"pk": str(self.worker_config.id)}),
                 data={
diff --git a/arkindex/process/tests/test_workeractivity.py b/arkindex/process/tests/test_workeractivity.py
index 0585e9cf52..4520f37c3a 100644
--- a/arkindex/process/tests/test_workeractivity.py
+++ b/arkindex/process/tests/test_workeractivity.py
@@ -799,10 +799,11 @@ class TestWorkerActivity(FixtureTestCase):
             response = self.client.get(reverse("api:corpus-activity", kwargs={"corpus": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_list_corpus_acl(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_list_corpus_acl(self, has_access_mock):
         private_corpus = Corpus.objects.create(name="can't touch this")
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:corpus-activity", kwargs={"corpus": private_corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have guest access to this corpus."})
@@ -896,7 +897,7 @@ class TestWorkerActivity(FixtureTestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.get(
                 reverse("api:corpus-activity", kwargs={"corpus": self.corpus.id}),
                 {"process_id": str(process.id)}
diff --git a/arkindex/process/tests/test_workeractivity_stats.py b/arkindex/process/tests/test_workeractivity_stats.py
index b7b9c51d1b..3e8e292a6e 100644
--- a/arkindex/process/tests/test_workeractivity_stats.py
+++ b/arkindex/process/tests/test_workeractivity_stats.py
@@ -1,6 +1,7 @@
 import itertools
 import uuid
 from datetime import datetime, timedelta, timezone
+from unittest.mock import patch
 
 from django.db.models.functions import Now
 from django.urls import reverse
@@ -121,9 +122,10 @@ class TestWorkerActivityStats(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
-    def test_corpus_private(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_corpus_private(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(
                 reverse("api:corpus-activity-stats", kwargs={"corpus": str(self.private_corpus.id)})
             )
@@ -148,7 +150,7 @@ class TestWorkerActivityStats(FixtureAPITestCase):
         user.save()
         self.private_corpus.memberships.create(user=user, level=Role.Guest.value)
         self.client.force_login(user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:corpus-activity-stats", kwargs={"corpus": str(self.private_corpus.id)})
             )
@@ -351,16 +353,17 @@ class TestWorkerActivityStats(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
-    def test_process_private(self):
+    @patch("arkindex.project.mixins.get_max_level", return_value=None)
+    def test_process_private(self, get_max_level_mock):
         # An inaccessible project
         self.process_1.corpus = self.private_corpus
         self.process_1.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:process-activity-stats", kwargs={"pk": str(self.process_1.id)})
             )
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
     def test_process_not_found(self):
         self.client.force_login(self.user)
@@ -372,7 +375,7 @@ class TestWorkerActivityStats(FixtureAPITestCase):
 
     def test_process(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:process-activity-stats", kwargs={"pk": str(self.process_1.id)})
             )
diff --git a/arkindex/process/tests/test_workerruns.py b/arkindex/process/tests/test_workerruns.py
index e66ecfc19f..d0a6f75d27 100644
--- a/arkindex/process/tests/test_workerruns.py
+++ b/arkindex/process/tests/test_workerruns.py
@@ -1,5 +1,7 @@
 import uuid
 from datetime import datetime, timezone
+from unittest import expectedFailure
+from unittest.mock import call, patch
 
 from django.test import override_settings
 from django.urls import reverse
@@ -110,7 +112,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.worker_1.memberships.all().delete()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.get(reverse("api:worker-run-list", kwargs={"pk": str(self.process_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -119,7 +121,7 @@ class TestWorkerRuns(FixtureAPITestCase):
     def test_list(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.get(reverse("api:worker-run-list", kwargs={"pk": str(self.process_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -175,7 +177,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.get(reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -194,14 +196,15 @@ class TestWorkerRuns(FixtureAPITestCase):
 
         self.assertDictEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
-    def test_create_no_execution_right(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_no_execution_right(self, has_access_mock):
         """
         An execution access on the target worker version is required to create a worker run
         """
         self.worker_1.memberships.update(level=Role.Guest.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={"worker_version_id": str(self.version_1.id), "parents": []},
@@ -211,11 +214,16 @@ class TestWorkerRuns(FixtureAPITestCase):
 
         self.assertEqual(response.json(), {"worker_version_id": ["You do not have an execution access to this worker."]})
 
+        self.assertListEqual(has_access_mock.call_args_list, [
+            call(self.user, self.worker_1, Role.Contributor.value, skip_public=False),
+            call(self.user, self.repo, Role.Contributor.value, skip_public=False),
+        ])
+
     def test_create_invalid_version(self):
         self.client.force_login(self.user)
         version_id = uuid.uuid4()
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={"worker_version_id": version_id},
@@ -243,8 +251,8 @@ class TestWorkerRuns(FixtureAPITestCase):
                 self.run_1.configuration = configuration
                 self.run_1.save()
 
-                # Having a model version set adds two queries, having a configuration adds one
-                query_count = 11 + bool(model_version) * 2 + bool(configuration)
+                # Having a model version or a configuration adds one query for each
+                query_count = 6 + bool(model_version) + bool(configuration)
 
                 with self.assertNumQueries(query_count):
                     response = self.client.post(
@@ -267,7 +275,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.version_1.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={"worker_version_id": str(self.version_1.id), "parents": []},
@@ -282,7 +290,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.worker_1.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={"worker_version_id": str(self.version_1.id), "parents": []},
@@ -299,7 +307,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.version_1.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={"worker_version_id": str(self.version_1.id), "model_version_id": str(self.model_version_1.id)},
@@ -330,7 +338,7 @@ class TestWorkerRuns(FixtureAPITestCase):
                 self.process_2.mode = mode
                 self.process_2.save()
 
-                with self.assertNumQueries(11):
+                with self.assertNumQueries(6):
                     response = self.client.post(
                         reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                         {"worker_version_id": str(self.version_1.id)},
@@ -369,7 +377,7 @@ class TestWorkerRuns(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(process.id)}),
                 data={"worker_version_id": str(self.version_1.id), "parents": []},
@@ -391,7 +399,7 @@ class TestWorkerRuns(FixtureAPITestCase):
                 self.process_2.mode = mode
                 self.process_2.save()
 
-                with self.assertNumQueries(13):
+                with self.assertNumQueries(8):
                     response = self.client.post(
                         reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                         {"worker_version_id": str(self.version_1.id), "parents": []},
@@ -453,7 +461,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.worker_1.repository.memberships.create(user=self.user, level=Role.Contributor.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={"worker_version_id": str(self.version_1.id), "parents": []},
@@ -467,7 +475,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)})
             )
@@ -480,7 +488,7 @@ class TestWorkerRuns(FixtureAPITestCase):
     def test_create_configuration(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(9):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={
@@ -549,7 +557,7 @@ class TestWorkerRuns(FixtureAPITestCase):
     def test_create_invalid_configuration(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={"worker_version_id": str(self.version_1.id), "parents": [], "configuration_id": str(self.configuration_2.id)},
@@ -589,7 +597,7 @@ class TestWorkerRuns(FixtureAPITestCase):
             docker_image_id=self.version_1.docker_image_id,
         )
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:worker-run-list", kwargs={"pk": str(self.process_2.id)}),
                 data={"worker_version_id": str(version.id), "parents": []},
@@ -658,7 +666,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.worker_1.memberships.update(level=Role.Guest.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.get(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)})
             )
@@ -720,7 +728,7 @@ class TestWorkerRuns(FixtureAPITestCase):
     def test_retrieve(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.get(reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -849,7 +857,7 @@ class TestWorkerRuns(FixtureAPITestCase):
 
         # Check that the gitrefs are retrieved with RetrieveWorkerRun
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.get(reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -963,10 +971,10 @@ class TestWorkerRuns(FixtureAPITestCase):
         """
         self.process_1.tasks.create(run=0, depth=0, slug="something", agent=self.agent)
 
-        with self.assertNumQueries(6):
+        self.client.force_login(self.user)
+        with self.assertNumQueries(7):
             response = self.client.get(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
             )
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -1014,23 +1022,25 @@ class TestWorkerRuns(FixtureAPITestCase):
             "summary": f"Worker Recognizer @ {str(self.version_1.id)[:6]}",
         })
 
+    @expectedFailure
     def test_retrieve_agent_unassigned(self):
         """
         A Ponos agent cannot retrieve a WorkerRun on a process where it does not have any assigned tasks
         """
         self.process_1.tasks.create(run=0, depth=0, slug="something", agent=None)
 
-        with self.assertNumQueries(2):
+        # Agent auth is not implemented in CE
+        self.client.force_login(self.user)
+        with self.assertNumQueries(3):
             response = self.client.get(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
             )
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
     def test_update_requires_nothing(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={},
@@ -1064,14 +1074,15 @@ class TestWorkerRuns(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_update_no_project_admin_right(self):
+    @patch("arkindex.project.mixins.get_max_level", return_value=Role.Contributor.value)
+    def test_update_no_project_admin_right(self, get_max_level_mock):
         """
-        A user cannot update a worker run if he has no admin access on its process project
+        A user cannot update a worker run if they have no admin access on its process project
         """
-        self.corpus.memberships.filter(user=self.user).update(level=Role.Guest.value)
+        self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -1082,6 +1093,9 @@ class TestWorkerRuns(FixtureAPITestCase):
 
         self.assertEqual(response.json(), {"detail": "You do not have an admin access to this process."})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.corpus))
+
     def test_update_invalid_process_mode(self):
         """
         A user cannot update a worker run on a local process
@@ -1132,7 +1146,7 @@ class TestWorkerRuns(FixtureAPITestCase):
     def test_update_nonexistent_parent(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -1152,7 +1166,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.client.force_login(self.user)
         run_2 = self.process_1.worker_runs.create(version=self.version_2)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -1188,7 +1202,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         Process field cannot be updated
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -1251,7 +1265,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -1313,7 +1327,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         # Check generated summary, before updating, it should not be that verbose
         self.assertEqual(self.run_1.summary, f"Worker Recognizer @ {str(self.run_1.version.id)[:6]}")
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": self.run_1.id}),
                 data={
@@ -1382,7 +1396,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.assertEqual(self.run_1.configuration, None)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": self.run_1.id}),
                 data={"parents": [], "configuration_id": str(self.configuration_2.id)},
@@ -1400,7 +1414,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.assertTrue(self.process_1.tasks.exists())
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -1435,7 +1449,7 @@ class TestWorkerRuns(FixtureAPITestCase):
             parents=[],
         )
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(run_2.id)}),
                 data={
@@ -1472,7 +1486,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         )
         random_model_version_uuid = str(uuid.uuid4())
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(run_2.id)}),
                 data={
@@ -1487,7 +1501,8 @@ class TestWorkerRuns(FixtureAPITestCase):
             "model_version_id": [f'Invalid pk "{random_model_version_uuid}" - object does not exist.']
         })
 
-    def test_update_model_version_no_access(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=ModelVersion.objects.none())
+    def test_update_model_version_no_access(self, filter_rights_mock):
         """
         Cannot update a worker run with a model_version UUID, when you don't have access to the model version
         """
@@ -1518,7 +1533,7 @@ class TestWorkerRuns(FixtureAPITestCase):
             archive_hash="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
         )
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(run_2.id)}),
                 data={
@@ -1533,7 +1548,13 @@ class TestWorkerRuns(FixtureAPITestCase):
             "model_version_id": [f'Invalid pk "{model_version_no_access.id}" - object does not exist.'],
         })
 
-    def test_update_model_version_guest(self):
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Model, Role.Guest.value),
+            call(self.user, Model, Role.Contributor.value),
+        ])
+
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_update_model_version_guest(self, filter_rights_mock):
         """
         Cannot update a worker run with a model_version when you only have guest access to the model,
         and the model version has no tag or is not available
@@ -1555,6 +1576,17 @@ class TestWorkerRuns(FixtureAPITestCase):
             parents=[],
         )
 
+        def filter_rights(user, model, level):
+            """
+            The filter_rights mock needs to return nothing when called for contributor access,
+            and the models we will test on when called for guest access
+            """
+            if level == Role.Guest.value:
+                return Model.objects.filter(id__in=(self.model_2.id, self.model_3.id))
+            return Model.objects.none()
+
+        filter_rights_mock.side_effect = filter_rights
+
         cases = [
             # On a model with a membership giving guest access
             (self.model_version_2, None, ModelVersionState.Created),
@@ -1571,12 +1603,13 @@ class TestWorkerRuns(FixtureAPITestCase):
         ]
 
         for model_version, tag, state in cases:
+            filter_rights_mock.reset_mock()
             with self.subTest(model_version=model_version, tag=tag, state=state):
                 model_version.tag = tag
                 model_version.state = state
                 model_version.save()
 
-                with self.assertNumQueries(9):
+                with self.assertNumQueries(5):
                     response = self.client.put(
                         reverse("api:worker-run-details", kwargs={"pk": str(run_2.id)}),
                         data={
@@ -1591,6 +1624,11 @@ class TestWorkerRuns(FixtureAPITestCase):
                     "model_version_id": [f'Invalid pk "{model_version.id}" - object does not exist.'],
                 })
 
+                self.assertListEqual(filter_rights_mock.call_args_list, [
+                    call(self.user, Model, Role.Guest.value),
+                    call(self.user, Model, Role.Contributor.value),
+                ])
+
     def test_update_model_version_unavailable(self):
         self.client.force_login(self.user)
         rev_2 = self.repo.revisions.create(
@@ -1611,7 +1649,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.model_version_1.state = ModelVersionState.Error
         self.model_version_1.save()
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(run.id)}),
                 data={
@@ -1643,7 +1681,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.model_1.archived = datetime.now(timezone.utc)
         self.model_1.save()
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(run.id)}),
                 data={
@@ -1691,7 +1729,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         ]
         for model_version in model_versions:
             with self.subTest(model_version=model_version):
-                with self.assertNumQueries(13):
+                with self.assertNumQueries(9):
                     response = self.client.put(
                         reverse("api:worker-run-details", kwargs={"pk": str(run.id)}),
                         data={
@@ -1784,7 +1822,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         # Check generated summary, before updating, there should be only information about the worker version
         self.assertEqual(run.summary, f"Worker {self.worker_1.name} @ {str(version_with_model.id)[:6]}")
 
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(10):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(run.id)}),
                 data={
@@ -1876,7 +1914,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -1957,8 +1995,8 @@ class TestWorkerRuns(FixtureAPITestCase):
                     configuration=None if configuration else self.configuration_1,
                 )
 
-                # Having a model version set adds two queries, having a configuration adds one
-                query_count = 8 + bool(model_version) * 2 + bool(configuration)
+                # Having a model version or a configuration adds one query for each
+                query_count = 5 + bool(model_version) + bool(configuration)
 
                 with self.assertNumQueries(query_count):
                     response = self.client.put(
@@ -1976,16 +2014,18 @@ class TestWorkerRuns(FixtureAPITestCase):
                     "__all__": ["A WorkerRun already exists on this process with the selected worker version, model version and configuration."],
                 })
 
+    @expectedFailure
     def test_update_agent(self):
         """
         Ponos agents cannot update WorkerRuns, even when they can access them
         """
         self.process_1.tasks.create(run=0, depth=0, slug="something", agent=self.agent)
 
-        with self.assertNumQueries(3):
+        # Agent auth is not implemented in CE
+        self.client.force_login(self.user)
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {
@@ -2018,14 +2058,15 @@ class TestWorkerRuns(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_partial_update_no_project_admin_right(self):
+    @patch("arkindex.project.mixins.get_max_level", return_value=Role.Contributor.value)
+    def test_partial_update_no_project_admin_right(self, get_max_level_mock):
         """
-        A user cannot update a worker run if he has no admin access on its process project
+        A user cannot update a worker run if they have no admin access on its process project
         """
-        self.corpus.memberships.filter(user=self.user).update(level=Role.Guest.value)
+        self.corpus.memberships.filter(user=self.user).update(level=Role.Contributor.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)})
             )
@@ -2033,6 +2074,9 @@ class TestWorkerRuns(FixtureAPITestCase):
 
         self.assertEqual(response.json(), {"detail": "You do not have an admin access to this process."})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.corpus))
+
     def test_partial_update_invalid_id(self):
         rev_2 = self.repo.revisions.create(
             hash="2",
@@ -2082,7 +2126,7 @@ class TestWorkerRuns(FixtureAPITestCase):
 
     def test_partial_update_nonexistent_parent(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -2104,7 +2148,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -2166,7 +2210,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -2227,7 +2271,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.assertEqual(self.run_1.configuration, None)
         self.assertEqual(self.run_1.summary, f"Worker {self.run_1.version.worker.name} @ {str(self.run_1.version.id)[:6]}")
 
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": self.run_1.id}),
                 data={
@@ -2293,7 +2337,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.assertEqual(self.run_1.configuration, None)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": self.run_1.id}),
                 data={"configuration_id": str(self.configuration_2.id)},
@@ -2311,7 +2355,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.assertTrue(self.process_1.tasks.exists())
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -2346,7 +2390,7 @@ class TestWorkerRuns(FixtureAPITestCase):
             parents=[],
         )
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(run_2.id)}),
                 data={
@@ -2382,7 +2426,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         )
         random_model_version_uuid = str(uuid.uuid4())
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(5):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(run_2.id)}),
                 data={
@@ -2396,7 +2440,8 @@ class TestWorkerRuns(FixtureAPITestCase):
             "model_version_id": [f'Invalid pk "{random_model_version_uuid}" - object does not exist.']
         })
 
-    def test_partial_update_model_version_no_access(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=ModelVersion.objects.none())
+    def test_partial_update_model_version_no_access(self, filter_rights_mock):
         """
         Cannot update a worker run with a model_version UUID, when you don't have access to the model version
         """
@@ -2421,7 +2466,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         model_no_access = Model.objects.create(name="Secret model")
         model_version_no_access = ModelVersion.objects.create(model=model_no_access, state=ModelVersionState.Available, size=8, hash="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", archive_hash="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(run_2.id)}),
                 data={
@@ -2435,7 +2480,13 @@ class TestWorkerRuns(FixtureAPITestCase):
             "model_version_id": [f'Invalid pk "{model_version_no_access.id}" - object does not exist.'],
         })
 
-    def test_partial_update_model_version_guest(self):
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Model, Role.Guest.value),
+            call(self.user, Model, Role.Contributor.value),
+        ])
+
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_partial_update_model_version_guest(self, filter_rights_mock):
         """
         Cannot update a worker run with a model_version when you only have guest access to the model,
         and the model version has no tag or is not available
@@ -2457,6 +2508,17 @@ class TestWorkerRuns(FixtureAPITestCase):
             parents=[],
         )
 
+        def filter_rights(user, model, level):
+            """
+            The filter_rights mock needs to return nothing when called for contributor access,
+            and the models we will test on when called for guest access
+            """
+            if level == Role.Guest.value:
+                return Model.objects.filter(id__in=(self.model_2.id, self.model_3.id))
+            return Model.objects.none()
+
+        filter_rights_mock.side_effect = filter_rights
+
         cases = [
             # On a model with a membership giving guest access
             (self.model_version_2, None, ModelVersionState.Created),
@@ -2473,12 +2535,13 @@ class TestWorkerRuns(FixtureAPITestCase):
         ]
 
         for model_version, tag, state in cases:
+            filter_rights_mock.reset_mock()
             with self.subTest(model_version=model_version, tag=tag, state=state):
                 model_version.tag = tag
                 model_version.state = state
                 model_version.save()
 
-                with self.assertNumQueries(9):
+                with self.assertNumQueries(5):
                     response = self.client.patch(
                         reverse("api:worker-run-details", kwargs={"pk": str(run_2.id)}),
                         data={
@@ -2492,6 +2555,11 @@ class TestWorkerRuns(FixtureAPITestCase):
                     "model_version_id": [f'Invalid pk "{model_version.id}" - object does not exist.'],
                 })
 
+                self.assertListEqual(filter_rights_mock.call_args_list, [
+                    call(self.user, Model, Role.Guest.value),
+                    call(self.user, Model, Role.Contributor.value),
+                ])
+
     def test_partial_update_model_version_unavailable(self):
         self.client.force_login(self.user)
         rev_2 = self.repo.revisions.create(
@@ -2512,7 +2580,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.model_version_1.state = ModelVersionState.Error
         self.model_version_1.save()
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(run.id)}),
                 data={
@@ -2544,7 +2612,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.model_1.archived = datetime.now(timezone.utc)
         self.model_1.save()
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(run.id)}),
                 data={
@@ -2591,7 +2659,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         ]
         for model_version in model_versions:
             with self.subTest(model_version=model_version):
-                with self.assertNumQueries(13):
+                with self.assertNumQueries(9):
                     response = self.client.patch(
                         reverse("api:worker-run-details", kwargs={"pk": str(run.id)}),
                         data={
@@ -2682,7 +2750,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         )
         self.assertEqual(run.model_version_id, None)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(9):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(run.id)}),
                 data={
@@ -2771,7 +2839,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
                 data={
@@ -2852,8 +2920,8 @@ class TestWorkerRuns(FixtureAPITestCase):
                     configuration=None if configuration else self.configuration_1,
                 )
 
-                # Having a model version set adds two queries, having a configuration adds one
-                query_count = 8 + bool(model_version) * 2 + bool(configuration)
+                # Having a model version or a configuration adds one query for each
+                query_count = 5 + bool(model_version) + bool(configuration)
 
                 with self.assertNumQueries(query_count):
                     response = self.client.patch(
@@ -2871,16 +2939,18 @@ class TestWorkerRuns(FixtureAPITestCase):
                     "__all__": ["A WorkerRun already exists on this process with the selected worker version, model version and configuration."],
                 })
 
+    @expectedFailure
     def test_partial_update_agent(self):
         """
         Ponos agents cannot update WorkerRuns, even when they can access them
         """
         self.process_1.tasks.create(run=0, depth=0, slug="something", agent=self.agent)
 
-        with self.assertNumQueries(3):
+        # Agent auth is not implemented in CE
+        self.client.force_login(self.user)
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {
@@ -2909,7 +2979,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.worker_1.memberships.update(level=Role.Guest.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)})
             )
@@ -2936,7 +3006,7 @@ class TestWorkerRuns(FixtureAPITestCase):
     def test_delete(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)})
             )
@@ -2979,7 +3049,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.assertTrue(self.run_1.id in run_3.parents)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)})
             )
@@ -3000,7 +3070,7 @@ class TestWorkerRuns(FixtureAPITestCase):
         self.process_1.run()
         self.process_1.tasks.update(state=State.Running)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)})
             )
@@ -3008,16 +3078,18 @@ class TestWorkerRuns(FixtureAPITestCase):
 
         self.assertEqual(response.json(), ["WorkerRuns cannot be deleted from a process that has already started."])
 
+    @expectedFailure
     def test_delete_agent(self):
         """
         Ponos agents cannot delete WorkerRuns, even when they can access them
         """
         self.process_1.tasks.create(run=0, depth=0, slug="something", agent=self.agent)
 
-        with self.assertNumQueries(2):
+        # Agent auth is not implemented in CE
+        self.client.force_login(self.user)
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:worker-run-details", kwargs={"pk": str(self.run_1.id)}),
-                HTTP_AUTHORIZATION=f"Bearer {self.agent.token.access_token}",
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {
diff --git a/arkindex/process/tests/test_workers.py b/arkindex/process/tests/test_workers.py
index bae4880741..98a2d31ffd 100644
--- a/arkindex/process/tests/test_workers.py
+++ b/arkindex/process/tests/test_workers.py
@@ -1,9 +1,11 @@
 import uuid
 from datetime import datetime, timezone
+from unittest.mock import call, patch
 
 from django.urls import reverse
 from rest_framework import status
 
+from arkindex.documents.models import Corpus
 from arkindex.ponos.models import Farm
 from arkindex.process.models import (
     CorpusWorkerVersion,
@@ -21,7 +23,7 @@ from arkindex.process.models import (
 )
 from arkindex.project.tests import FixtureAPITestCase
 from arkindex.training.models import Model, ModelVersionState
-from arkindex.users.models import Right, Role, User
+from arkindex.users.models import Role, User
 
 
 class TestWorkersWorkerVersions(FixtureAPITestCase):
@@ -88,12 +90,14 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
 
     def test_workers_list(self):
         """
-        User is able to list workers on the repository as he has an admin access
+        User is able to list workers on the repository as they have an admin access
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:workers-list"))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "count": 6,
             "next": None,
@@ -163,14 +167,15 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         """
         self.model.memberships.create(user=self.user, level=Role.Guest.value)
         self.worker_generic.models.set([self.model])
-
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:workers-list"),
                 {"compatible_model": str(self.model.id)},
             )
             self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "count": 1,
             "next": None,
@@ -192,14 +197,15 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
     def test_workers_list_compatible_model_public(self):
         public_model = Model.objects.create(name="Public", public=True)
         self.worker_reco.models.set([public_model])
-
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:workers-list"),
                 {"compatible_model": str(public_model.id)},
             )
             self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertEqual(response.json(), {
             "count": 1,
             "next": None,
@@ -216,22 +222,37 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             }],
         })
 
-    def test_worker_list_compatible_model_private(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_worker_list_compatible_model_private(self, filter_rights_mock):
         private_model = Model.objects.create(name="Private")
         self.worker_reco.models.set([private_model])
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        filter_rights_mock.side_effect = [
+            Worker.objects.all(),
+            Repository.objects.all(),
+            Model.objects.none(),
+        ]
+
+        with self.assertNumQueries(2):
             response = self.client.get(
                 reverse("api:workers-list"),
                 {"compatible_model": str(private_model.id)},
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertDictEqual(response.json(), {
             "compatible_model": ["This model does not exist or you don't have access to it"]
         })
 
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+            call(self.user, Model, Role.Guest.value),
+        ])
+
     def test_workers_list_filter_invalid_query_params(self):
         self.client.force_login(self.user)
+
         with self.assertNumQueries(2):
             response = self.client.get(
                 reverse("api:workers-list"),
@@ -241,6 +262,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 },
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertEqual(response.json(), {
             "compatible_model": ["Invalid UUID"],
             "repository_id": ["Invalid UUID"],
@@ -251,9 +273,11 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         User is able to filter workers on the repository by worker type slug
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:workers-list"), {"type": "dla"})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "count": 1,
             "next": None,
@@ -277,7 +301,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.worker_dla.archived = datetime.now(timezone.utc)
         self.worker_dla.save()
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:workers-list"), {"archived": True})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -305,7 +329,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.worker_dla.archived = None
         self.worker_dla.save()
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:workers-list"), {"archived": False})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -332,9 +356,11 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         Raise when trying to filter workers with an invalid type slug
         """
         self.client.force_login(self.user)
+
         with self.assertNumQueries(3):
             response = self.client.get(reverse("api:workers-list"), {"type": "invalid-slug"})
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertDictEqual(response.json(), {"type": ["No registered worker type with that slug."]})
 
     def test_workers_list_filter_type_id(self):
@@ -342,9 +368,11 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         User is able to filter workers on the repository by worker type id
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:workers-list"), {"type": self.worker_type_dla.id})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "count": 1,
             "next": None,
@@ -363,16 +391,21 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             ]
         })
 
-    def test_workers_list_requires_contributor(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_workers_list_requires_contributor(self, filter_rights_mock):
         """
         User is not able to list workers with a guest access
         """
-        self.repo.memberships.filter(user=self.user).update(level=Role.Guest.value)
-        self.worker_custom.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(4):
+        filter_rights_mock.side_effect = [
+            Worker.objects.none(),
+            Repository.objects.none(),
+        ]
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:workers-list"))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "count": 0,
             "next": None,
@@ -381,40 +414,18 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             "results": []
         })
 
-    def test_workers_list_public_attribute(self):
-        """
-        Any authenticated user is able to list a worker with the public attribute
-        """
-        user = User.objects.create_user("user42@test.fr", "abcd")
-        user.verified_email = True
-        user.save()
-        self.worker_reco.public = True
-        self.worker_reco.save()
-        self.client.force_login(user)
-        with self.assertNumQueries(7):
-            response = self.client.get(reverse("api:workers-list"))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "count": 1,
-            "next": None,
-            "number": 1,
-            "previous": None,
-            "results": [{
-                "id": str(self.worker_reco.id),
-                "repository_id": str(self.repo.id),
-                "name": "Recognizer",
-                "description": "",
-                "slug": "reco",
-                "type": "recognizer",
-                "archived": False,
-            }]
-        })
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
 
     def test_workers_list_name_filter(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:workers-list"), {"name": "layout"})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "count": 1,
             "next": None,
@@ -444,10 +455,12 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             type=self.worker_type_dla
         )
         worker_2.memberships.create(user=self.user, level=Role.Contributor.value)
-
         self.client.force_login(self.user)
-        response = self.client.get(reverse("api:workers-list"), {"repository_id": str(repo2.id)})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+        with self.assertNumQueries(4):
+            response = self.client.get(reverse("api:workers-list"), {"repository_id": str(repo2.id)})
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "count": 1,
             "next": None,
@@ -479,11 +492,12 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         )
         worker_2 = repo2.workers.create(name="Worker 2", slug="worker_2", type=self.worker_type_dla)
         repo2.memberships.create(user=self.user, level=Role.Contributor.value)
-
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:worker-retrieve", kwargs={"pk": str(worker_2.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "id": str(worker_2.id),
             "repository_id": str(repo2.id),
@@ -496,9 +510,11 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
 
     def test_workers_retrieve_no_revision(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_custom.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "id": str(self.worker_custom.id),
             "repository_id": None,
@@ -509,46 +525,50 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             "archived": False,
         })
 
-    def test_workers_retrieve_no_rights(self):
-        new_user = User.objects.create(email="new@test.fr", verified_email=True)
-        self.client.force_login(new_user)
-        with self.assertNumQueries(6):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_workers_retrieve_no_rights(self, filter_rights_mock):
+        filter_rights_mock.side_effect = [
+            Worker.objects.none(),
+            Repository.objects.none(),
+        ]
+        self.client.force_login(self.user)
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-        self.assertEqual(response.json(), {"detail": "Not found."})
 
-    def test_workers_retrieve_guest(self):
-        """
-        Users cannot retrieve workers with guest access, as guest has no meaning on workers
-        """
-        self.client.force_login(self.user)
-        self.worker_custom.memberships.filter(user=self.user).update(level=Role.Guest.value)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_custom.id)}))
-            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertEqual(response.json(), {"detail": "Not found."})
 
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
+
     def test_worker_create_requires_login(self):
         with self.assertNumQueries(0):
             response = self.client.post(reverse("api:workers-list"))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
     def test_worker_create_requires_verified(self):
         self.user.verified_email = False
         self.user.save()
         self.client.force_login(self.user)
+
         with self.assertNumQueries(2):
             response = self.client.post(reverse("api:workers-list"))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
     def test_worker_create_required_fields(self):
         self.client.force_login(self.user)
+
         with self.assertNumQueries(2):
             response = self.client.post(reverse("api:workers-list"))
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
             "name": ["This field is required."],
             "slug": ["This field is required."],
@@ -559,7 +579,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         """
         Repository ID is deduced from the authenticated task.
         """
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:workers-list"),
                 data={
@@ -571,6 +591,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
             )
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
         worker = Worker.objects.get(id=response.json()["id"])
         assert worker.repository_id == self.repo.id
 
@@ -590,6 +611,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 }
             )
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
         data = response.json()
         worker = Worker.objects.get(id=data["id"])
         self.assertEqual(worker.type, self.worker_type_dla)
@@ -607,14 +629,16 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             "archived": False,
         })
 
-    def test_worker_create_user_existing_no_execution_access(self):
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Guest.value)
+    def test_worker_create_user_existing_no_execution_access(self, get_max_level_mock):
         """
-        A user cannot retrieve a worker without an execution access in case the slug already exist.
+        A user cannot retrieve a worker without an execution access in case the slug already exists.
         No worker type is created.
         """
         self.worker_custom.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:workers-list"),
                 data={
@@ -624,19 +648,23 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 }
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertFalse(WorkerType.objects.filter(slug="new_type").exists())
         self.assertQuerysetEqual(self.worker_custom.memberships.values_list("user_id", "level"), [
             (self.user.id, Role.Guest.value)
         ])
         self.assertDictEqual(response.json(), {"slug": ["You cannot use this value."]})
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.worker_custom))
 
     def test_worker_create_task_repository_process(self):
         """
-        Ponos task can create a worker only on a process of Repository type
+        Ponos tasks can create a worker only on a process of Repository type
         """
         process = Process.objects.get(mode=ProcessMode.Workers)
         process.run()
         task = process.tasks.first()
+
         with self.assertNumQueries(1):
             response = self.client.post(
                 reverse("api:workers-list"),
@@ -648,6 +676,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 HTTP_AUTHORIZATION=f"Ponos {task.token}",
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {
             "detail": (
                 "Workers can only be created on processes of type Repository with a"
@@ -655,15 +684,12 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             )
         })
 
-    def test_worker_create_task_rights(self):
+    @patch("arkindex.process.serializers.workers.get_max_level", return_value=None)
+    def test_worker_create_task_rights(self, get_max_level_mock):
         """
         Ponos tasks are not allowed to create workers when they do not have a right on the repository
         """
-        Right.objects.filter(
-            content_id__in=(self.repo.id, *self.repo.workers.values_list("id", flat=True)),
-            user=self.user
-        ).delete()
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:workers-list"),
                 data={
@@ -676,11 +702,14 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.repo))
+
     def test_worker_create_task_return_existing_worker(self):
         """
         Creation with an existing slug returns the existing worker with a HTTP status 200
         """
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:workers-list"),
                 data={
@@ -692,6 +721,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
             )
             self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         data = response.json()
         self.assertEqual(data["id"], str(self.worker_reco.id))
         self.assertEqual(data["name"], "Recognizer")
@@ -704,7 +734,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         """
         new_worker_type_slug = "newType"
         self.assertFalse(WorkerType.objects.filter(slug=new_worker_type_slug).exists())
-        with self.assertNumQueries(14):
+
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:workers-list"),
                 data={
@@ -715,6 +746,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
             )
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
         data = response.json()
         self.assertTrue(WorkerType.objects.filter(slug=new_worker_type_slug).exists())
         self.assertNotEqual(data["id"], str(self.worker_reco.id))
@@ -734,8 +766,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "archived": False,
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
     def test_update_requires_verified(self):
@@ -754,16 +786,21 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "archived": False,
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_update_requires_contributor(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_update_requires_contributor(self, filter_rights_mock):
         self.worker_reco.memberships.update_or_create(user=self.user, defaults={"level": Role.Guest.value})
         self.repo.memberships.update_or_create(user=self.user, defaults={"level": Role.Guest.value})
         self.client.force_login(self.user)
+        filter_rights_mock.side_effect = [
+            Worker.objects.none(),
+            Repository.objects.none(),
+        ]
 
-        with self.assertNumQueries(4):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                 {
@@ -774,16 +811,23 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "archived": False,
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertEqual(response.json(), {"detail": "Not found."})
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
 
-    def test_update(self):
-        self.repo.memberships.filter(user=self.user).delete()
-        self.worker_reco.memberships.update_or_create(user=self.user, defaults={"level": Role.Contributor.value})
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_update(self, filter_rights_mock):
         self.client.force_login(self.user)
+        filter_rights_mock.side_effect = [
+            Worker.objects.all(),
+            Repository.objects.none(),
+        ]
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.put(
                 reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                 {
@@ -794,8 +838,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "archived": False,
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
 
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json(), {
             "id": str(self.worker_reco.id),
             "name": "New name",
@@ -813,12 +857,20 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.assertEqual(self.worker_reco.type, self.worker_type_dla)
         self.assertEqual(self.worker_reco.repository_id, self.repo.id)
 
-    def test_update_repository_contributor(self):
-        self.worker_reco.memberships.filter(user=self.user).delete()
-        self.repo.memberships.update_or_create(user=self.user, defaults={"level": Role.Contributor.value})
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
+
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_update_repository_contributor(self, filter_rights_mock):
         self.client.force_login(self.user)
+        filter_rights_mock.side_effect = [
+            Worker.objects.none(),
+            Repository.objects.all(),
+        ]
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.put(
                 reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                 {
@@ -829,8 +881,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "archived": False,
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
 
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json(), {
             "id": str(self.worker_reco.id),
             "name": "New name",
@@ -848,10 +900,15 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.assertEqual(self.worker_reco.type, self.worker_type_dla)
         self.assertEqual(self.worker_reco.repository_id, self.repo.id)
 
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
+
     def test_update_unique_slug(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                 {
@@ -862,13 +919,14 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "archived": False,
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertEqual(response.json(), {
             "non_field_errors": ["A worker with the same repository and slug already exists."],
         })
 
-    def test_update_archived_requires_archivable(self):
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Contributor.value)
+    def test_update_archived_requires_archivable(self, get_max_level_mock):
         self.client.force_login(self.user)
         self.assertFalse(self.worker_reco.is_archivable(self.user))
 
@@ -879,10 +937,11 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
 
         for current_value, new_value in cases:
             with self.subTest(current_value=current_value, new_value=new_value):
+                get_max_level_mock.reset_mock()
                 self.worker_reco.archived = current_value
                 self.worker_reco.save()
 
-                with self.assertNumQueries(10):
+                with self.assertNumQueries(4):
                     response = self.client.put(
                         reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                         {
@@ -898,6 +957,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 self.assertEqual(response.json(), {
                     "archived": ["You are not allowed to archive or unarchive this worker."],
                 })
+                self.assertEqual(get_max_level_mock.call_count, 2)
+                self.assertListEqual(get_max_level_mock.call_args_list, [call(self.user, self.worker_reco)] * 2)
 
     def test_update_archived(self):
         self.client.force_login(self.user)
@@ -914,7 +975,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 self.worker_reco.archived = current_value
                 self.worker_reco.save()
 
-                with self.assertNumQueries(11):
+                with self.assertNumQueries(6):
                     response = self.client.put(
                         reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                         {
@@ -945,8 +1006,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "description": "New description",
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
     def test_partial_update_requires_verified(self):
@@ -961,32 +1022,47 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "description": "New description",
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_partial_update_requires_contributor(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_partial_update_requires_contributor(self, filter_rights_mock):
         self.worker_reco.memberships.update_or_create(user=self.user, defaults={"level": Role.Guest.value})
         self.repo.memberships.update_or_create(user=self.user, defaults={"level": Role.Guest.value})
         self.client.force_login(self.user)
+        filter_rights_mock.side_effect = [
+            Worker.objects.none(),
+            Repository.objects.none(),
+        ]
 
-        with self.assertNumQueries(4):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                 {
                     "description": "New description",
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertEqual(response.json(), {"detail": "Not found."})
 
-    def test_partial_update(self):
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
+
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_partial_update(self, filter_rights_mock):
         self.repo.memberships.filter(user=self.user).delete()
         self.worker_reco.memberships.update_or_create(user=self.user, defaults={"level": Role.Contributor.value})
         self.client.force_login(self.user)
+        filter_rights_mock.side_effect = [
+            Worker.objects.all(),
+            Repository.objects.none(),
+        ]
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                 {
@@ -994,8 +1070,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "type": "dla",
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
 
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json(), {
             "id": str(self.worker_reco.id),
             "name": "Recognizer",
@@ -1013,12 +1089,22 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.assertEqual(self.worker_reco.type, self.worker_type_dla)
         self.assertEqual(self.worker_reco.repository_id, self.repo.id)
 
-    def test_partial_update_repository_contributor(self):
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
+
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_partial_update_repository_contributor(self, filter_rights_mock):
         self.worker_reco.memberships.filter(user=self.user).delete()
         self.repo.memberships.update_or_create(user=self.user, defaults={"level": Role.Contributor.value})
         self.client.force_login(self.user)
+        filter_rights_mock.side_effect = [
+            Worker.objects.none(),
+            Repository.objects.all(),
+        ]
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.patch(
                 reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                 {
@@ -1026,8 +1112,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                     "slug": "new_slug",
                 },
             )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
 
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.json(), {
             "id": str(self.worker_reco.id),
             "name": "New name",
@@ -1045,10 +1131,15 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.assertEqual(self.worker_reco.type.slug, "recognizer")
         self.assertEqual(self.worker_reco.repository_id, self.repo.id)
 
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
+
     def test_partial_update_unique_slug(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                 {
@@ -1061,7 +1152,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             "non_field_errors": ["A worker with the same repository and slug already exists."],
         })
 
-    def test_partial_update_archived_requires_archivable(self):
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Contributor.value)
+    def test_partial_update_archived_requires_archivable(self, get_max_level_mock):
         self.client.force_login(self.user)
         self.assertFalse(self.worker_reco.is_archivable(self.user))
 
@@ -1072,10 +1164,11 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
 
         for current_value, new_value in cases:
             with self.subTest(current_value=current_value, new_value=new_value):
+                get_max_level_mock.reset_mock()
                 self.worker_reco.archived = current_value
                 self.worker_reco.save()
 
-                with self.assertNumQueries(9):
+                with self.assertNumQueries(3):
                     response = self.client.patch(
                         reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                         {
@@ -1087,6 +1180,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 self.assertEqual(response.json(), {
                     "archived": ["You are not allowed to archive or unarchive this worker."],
                 })
+                self.assertEqual(get_max_level_mock.call_count, 2)
+                self.assertListEqual(get_max_level_mock.call_args_list, [call(self.user, self.worker_reco)] * 2)
 
     def test_partial_update_archived(self):
         self.client.force_login(self.user)
@@ -1103,7 +1198,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 self.worker_reco.archived = current_value
                 self.worker_reco.save()
 
-                with self.assertNumQueries(10):
+                with self.assertNumQueries(5):
                     response = self.client.patch(
                         reverse("api:worker-retrieve", kwargs={"pk": str(self.worker_reco.id)}),
                         {
@@ -1126,7 +1221,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
     def test_versions_list_requires_login(self):
         with self.assertNumQueries(0):
             response = self.client.get(reverse("api:worker-versions", kwargs={"pk": str(self.worker_reco.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
     def test_versions_list(self):
         self.client.force_login(self.user)
@@ -1139,9 +1234,11 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         )
         last_version.created = "1999-09-09T09:09:09.090909Z"
         last_version.save()
-        with self.assertNumQueries(14):
+
+        with self.assertNumQueries(9):
             response = self.client.get(reverse("api:worker-versions", kwargs={"pk": str(self.worker_reco.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         data = response.json()
         self.assertEqual(data["count"], 2)
         # The revision created in this test has a recent timestamp so it is displayed first
@@ -1168,23 +1265,25 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             "created": "1999-09-09T09:09:09.090909Z",
         })
 
-    def test_workers_versions_list_requires_contributor(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_versions_list_requires_contributor(self, has_access_mock):
         """
         User is not able to list worker versions with a guest access
         """
-        self.repo.memberships.filter(user=self.user).update(level=Role.Guest.value)
-        self.worker_custom.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(4):
-            response = self.client.get(reverse("api:workers-list"))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+        with self.assertNumQueries(3):
+            response = self.client.get(reverse("api:worker-versions", kwargs={"pk": str(self.worker_reco.id)}))
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {
-            "count": 0,
-            "next": None,
-            "number": 1,
-            "previous": None,
-            "results": []
+            "detail": "You do not have an execution access to this worker.",
         })
+        self.assertEqual(has_access_mock.call_count, 2)
+        self.assertEqual(has_access_mock.call_args_list, [
+            call(self.user, self.worker_reco, Role.Contributor.value, skip_public=False),
+            call(self.user, self.repo, Role.Contributor.value, skip_public=False),
+        ])
 
     def test_versions_list_public_worker(self):
         """
@@ -1196,9 +1295,11 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.worker_reco.public = True
         self.worker_reco.save()
         self.client.force_login(user)
+
         with self.assertNumQueries(9):
             response = self.client.get(reverse("api:worker-versions", kwargs={"pk": str(self.worker_reco.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         data = response.json()
         self.assertEqual(data["count"], 1)
         self.assertEqual(data["results"][0]["id"], str(self.worker_reco.versions.first().id))
@@ -1218,9 +1319,10 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             gpu_usage=FeatureUsage.Disabled
         )
 
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(9):
             response = self.client.get(reverse("api:worker-versions", kwargs={"pk": str(worker_2.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         data = response.json()
         self.assertEqual(len(data["results"]), 1)
         version = data["results"][0]
@@ -1255,29 +1357,29 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         ])
 
         # Complete mode
-        with self.assertNumQueries(13):
+        with self.subTest(mode="complete"), self.assertNumQueries(8):
             response = self.client.get(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_reco.id)}),
                 {"mode": "complete"},
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
             )
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.json()["count"], 6)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.json()["count"], 6)
 
         # Simple mode
-        with self.assertNumQueries(10):
+        with self.subTest(mode="simple"), self.assertNumQueries(8):
             response = self.client.get(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_reco.id)}),
                 {"mode": "simple"},
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
             )
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        data = response.json()
-        self.assertEqual(data["count"], 3)
-        self.assertCountEqual(
-            [version["revision"]["id"] for version in data["results"]],
-            [str(tagged_rev.id), str(master_rev.id), str(main_rev.id)]
-        )
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+            data = response.json()
+            self.assertEqual(data["count"], 3)
+            self.assertCountEqual(
+                [version["revision"]["id"] for version in data["results"]],
+                [str(tagged_rev.id), str(master_rev.id), str(main_rev.id)]
+            )
 
     def test_create_version_non_existing_worker(self):
         with self.assertNumQueries(2):
@@ -1385,7 +1487,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.worker_custom.memberships.filter(user=self.user).update(level=Role.Admin.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_custom.id)}),
                 data={"revision_id": str(self.rev2.id), "configuration": {"test": "test2"}, "model_usage": FeatureUsage.Required.value},
@@ -1401,7 +1503,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.worker_custom.memberships.filter(user=self.user).update(level=Role.Admin.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_reco.id)}),
                 data={"configuration": {"test": "test2"}, "model_usage": FeatureUsage.Required.value},
@@ -1417,7 +1519,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.worker_custom.memberships.filter(user=self.user).update(level=Role.Admin.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_custom.id)}),
                 data={"revision_id": str(self.rev2.id), "configuration": {"test": "test2"}, "model_usage": FeatureUsage.Required.value},
@@ -1434,6 +1536,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         Ponos Task auth cannot create a version on a worker that is not linked to a repository.
         """
         self.worker_custom.versions.create(version=41, configuration={})
+
         with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_custom.id)}),
@@ -1442,24 +1545,31 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
             )
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
         self.assertDictEqual(response.json(), {
             "revision_id": ["Task authentication requires to create a version on workers linked to a repository."]
         })
 
-    def test_create_version_user_auth_requires_admin(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_version_user_auth_requires_admin(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_custom.id)}),
                 data={"configuration": {"test": "val"}, "model_usage": FeatureUsage.Required.value},
                 format="json",
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have an admin access to this worker."})
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.worker_custom, Role.Admin.value, skip_public=False))
+
     def test_create_version_user_auth_requires_null_repository(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_dla.id)}),
                 data={"revision_id": str(self.rev2.id), "configuration": {"test": "test2"}, "model_usage": FeatureUsage.Required.value},
@@ -1478,7 +1588,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.worker_custom.memberships.filter(user=self.user).update(level=Role.Admin.value)
         self.worker_custom.versions.create(version=41, configuration={})
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_custom.id)}),
                 data={"configuration": {"test": "val"}, "model_usage": FeatureUsage.Required.value},
@@ -1512,7 +1622,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         Configuration body must be an object
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:worker-versions", kwargs={"pk": str(self.worker_reco.id)}),
                 data={"configuration": "test"},
@@ -2483,7 +2593,8 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             }]
         })
 
-    def test_corpus_worker_version_no_login_private(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.filter(public=True))
+    def test_corpus_worker_version_no_login_private(self, filter_rights_mock):
         self.corpus.public = False
         self.corpus.save()
         self.corpus.worker_versions.set([self.version_1, self.version_2])
@@ -2502,7 +2613,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         corpus_worker_version_1 = self.corpus.worker_version_cache.create(worker_version=self.version_1)
         corpus_worker_version_2 = self.corpus.worker_version_cache.create(worker_version=self.version_2)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.get(reverse("api:corpus-versions", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -2594,7 +2705,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
             worker_configuration=conf_2,
         )
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.get(reverse("api:corpus-versions", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -2703,7 +2814,7 @@ class TestWorkersWorkerVersions(FixtureAPITestCase):
         self.client.force_login(self.user)
         corpus_worker_version = self.corpus.worker_version_cache.create(worker_version=self.version_1)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(6):
             response = self.client.get(reverse("api:corpus-versions", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
diff --git a/arkindex/process/utils.py b/arkindex/process/utils.py
index 6bfc0e65a6..6928d65b8c 100644
--- a/arkindex/process/utils.py
+++ b/arkindex/process/utils.py
@@ -1,7 +1,6 @@
 import json
 from hashlib import md5
 
-from django.conf import settings
 from django.db.models import CharField, Value
 from django.db.models.functions import Cast, Concat, NullIf
 
@@ -11,24 +10,7 @@ __default_farm = None
 
 
 def get_default_farm():
-    """
-    Return the default Ponos Farm.
-    If defined, use PONOS_DEFAULT_FARM setting.
-    Otherwise, pick the first existing farm or create it.
-
-    This method uses a global variable to store the cached value retrieved in a idempotent way.
-    It could return an invalid farm in case the default farm is removed or updated during runtime.
-    """
-    from arkindex.ponos.models import Farm
-
-    global __default_farm
-    if __default_farm is not None:
-        return __default_farm
-    if settings.PONOS_DEFAULT_FARM:
-        __default_farm = Farm.objects.get(pk=settings.PONOS_DEFAULT_FARM)
-    else:
-        __default_farm = Farm.objects.order_by("name").first() or Farm.objects.create(name="Default farm")
-    return __default_farm
+    return None
 
 
 def hash_object(object):
diff --git a/arkindex/project/api_v1.py b/arkindex/project/api_v1.py
index 616d44640b..17c92d941b 100644
--- a/arkindex/project/api_v1.py
+++ b/arkindex/project/api_v1.py
@@ -59,22 +59,7 @@ from arkindex.documents.api.ml import (
 )
 from arkindex.documents.api.search import CorpusSearch, SearchIndexBuild
 from arkindex.images.api import IIIFInformationCreate, IIIFURLCreate, ImageCreate, ImageElements, ImageRetrieve
-from arkindex.ponos.api import (
-    AgentActions,
-    AgentDetails,
-    AgentRegister,
-    AgentsState,
-    AgentTokenRefresh,
-    FarmList,
-    PublicKeyEndpoint,
-    SecretDetails,
-    TaskArtifactDownload,
-    TaskArtifacts,
-    TaskCreate,
-    TaskDefinition,
-    TaskDetailsFromAgent,
-    TaskUpdate,
-)
+from arkindex.ponos.api import TaskArtifactDownload, TaskArtifacts, TaskDetailsFromAgent, TaskUpdate
 from arkindex.process.api import (
     ApplyProcessTemplate,
     BucketList,
@@ -136,19 +121,13 @@ from arkindex.training.api import (
     ValidateModelVersion,
 )
 from arkindex.users.api import (
-    GenericMembershipCreate,
-    GenericMembershipsList,
-    GroupDetails,
-    GroupsCreate,
     JobList,
     JobRetrieve,
-    MembershipDetails,
     PasswordReset,
     PasswordResetConfirm,
     UserCreate,
     UserEmailLogin,
     UserEmailVerification,
-    UserMemberships,
     UserRetrieve,
 )
 
@@ -326,14 +305,6 @@ api = [
     path("user/password-reset/", PasswordReset.as_view(), name="password-reset"),
     path("user/password-reset/confirm/", PasswordResetConfirm.as_view(), name="password-reset-confirm"),
 
-    # Rights management
-    path("groups/", GroupsCreate.as_view(), name="groups-create"),
-    path("group/<uuid:pk>/", GroupDetails.as_view(), name="group-details"),
-    path("user/memberships/", UserMemberships.as_view(), name="user-memberships"),
-    path("membership/<uuid:pk>/", MembershipDetails.as_view(), name="membership-details"),
-    path("memberships/", GenericMembershipsList.as_view(), name="memberships-list"),
-    path("memberships/create/", GenericMembershipCreate.as_view(), name="membership-create"),
-
     # Asynchronous jobs
     path("jobs/", JobList.as_view(), name="jobs-list"),
     path("jobs/<path:pk>/", JobRetrieve.as_view(), name="jobs-retrieve"),
@@ -342,18 +313,12 @@ api = [
     path("openapi/", OpenApiSchemaView.as_view(), name="openapi-schema"),
 
     # Ponos
-    path("task/", TaskCreate.as_view(), name="task-create"),
     path("task/<uuid:pk>/", TaskUpdate.as_view(), name="task-update"),
     path(
         "task/<uuid:pk>/from-agent/",
         TaskDetailsFromAgent.as_view(),
         name="task-details",
     ),
-    path(
-        "task/<uuid:pk>/definition/",
-        TaskDefinition.as_view(),
-        name="task-definition",
-    ),
     path(
         "task/<uuid:pk>/artifacts/", TaskArtifacts.as_view(), name="task-artifacts"
     ),
@@ -362,12 +327,4 @@ api = [
         TaskArtifactDownload.as_view(),
         name="task-artifact-download",
     ),
-    path("agent/", AgentRegister.as_view(), name="agent-register"),
-    path("agent/<uuid:pk>/", AgentDetails.as_view(), name="agent-details"),
-    path("agent/refresh/", AgentTokenRefresh.as_view(), name="agent-token-refresh"),
-    path("agent/actions/", AgentActions.as_view(), name="agent-actions"),
-    path("agents/", AgentsState.as_view(), name="agents-state"),
-    path("public-key/", PublicKeyEndpoint.as_view(), name="public-key"),
-    path("secret/<path:name>", SecretDetails.as_view(), name="secret-details"),
-    path("farms/", FarmList.as_view(), name="farm-list"),
 ]
diff --git a/arkindex/project/checks.py b/arkindex/project/checks.py
index f77237a4d9..4e9845fcb7 100644
--- a/arkindex/project/checks.py
+++ b/arkindex/project/checks.py
@@ -78,23 +78,6 @@ def local_imageserver_check(*args, **kwargs):
     return []
 
 
-@register()
-@only_runserver
-def ponos_key_check(*args, **kwargs):
-    """
-    Warn about a missing Ponos private key that would prevent any Ponos agent from authenticating
-    """
-    from django.conf import settings
-    if not os.path.exists(settings.PONOS_PRIVATE_KEY):
-        return [Warning(
-            f"Ponos private key at {settings.PONOS_PRIVATE_KEY} not found. "
-            "Agents will be unable to connect to this server.",
-            hint=f"`ponos.private_key` in {settings.CONFIG_PATH}",
-            id="arkindex.W007",
-        )]
-    return []
-
-
 @register()
 def ponos_env_check(*args, **kwargs):
     """
diff --git a/arkindex/project/config.py b/arkindex/project/config.py
index c57204f570..8e373583ac 100644
--- a/arkindex/project/config.py
+++ b/arkindex/project/config.py
@@ -7,7 +7,6 @@ updating the configuration documentation on the wiki:
 """
 import uuid
 from enum import Enum
-from pathlib import Path
 from typing import Optional
 from urllib.parse import urlparse
 
@@ -154,6 +153,8 @@ def get_settings_parser(base_dir):
     job_timeouts_parser.add_option("process_delete", type=int, default=3600)
     job_timeouts_parser.add_option("reindex_corpus", type=int, default=7200)
     job_timeouts_parser.add_option("notify_process_completion", type=int, default=120)
+    # Task execution in RQ timeouts after 10 hours by default
+    job_timeouts_parser.add_option("task", type=int, default=36000)
 
     csrf_parser = parser.add_subparser("csrf", default={})
     csrf_parser.add_option("cookie_name", type=str, default="arkindex.csrf")
@@ -177,9 +178,7 @@ def get_settings_parser(base_dir):
 
     ponos_parser = parser.add_subparser("ponos", default={})
     # Do not use file_path here to allow the backend to start without a Ponos key
-    ponos_parser.add_option("private_key", type=Path, default=(base_dir / "ponos.key").resolve())
     ponos_parser.add_option("default_env", type=dict, default={})
-    ponos_parser.add_option("default_farm", type=uuid.UUID, default=None)
     ponos_parser.add_option("artifact_max_size", type=int, default=5 * 1024**3)
 
     docker_parser = parser.add_subparser("docker", default={})
diff --git a/arkindex/project/mixins.py b/arkindex/project/mixins.py
index 6276b450d8..b26f626dd6 100644
--- a/arkindex/project/mixins.py
+++ b/arkindex/project/mixins.py
@@ -11,7 +11,7 @@ from arkindex.documents.models import Corpus
 from arkindex.process.models import Process, ProcessMode, Repository, Worker
 from arkindex.training.models import Model
 from arkindex.users.models import Role
-from arkindex.users.utils import check_level_param, filter_rights, get_max_level
+from arkindex.users.utils import filter_rights, get_max_level, has_access
 
 
 class ACLMixin(object):
@@ -29,33 +29,7 @@ class ACLMixin(object):
         return self._user or self.request.user
 
     def has_access(self, instance, level, skip_public=False):
-        """
-        Check if the user has access to a generic instance with a minimum level
-        If skip_public parameter is set to true, exclude rights on public instances
-        """
-        check_level_param(level)
-
-        # Handle special authentications
-        if not skip_public and level <= Role.Guest.value and getattr(instance, "public", False):
-            return True
-        if self.user.is_anonymous:
-            return False
-        elif self.user.is_admin:
-            return True
-
-        return instance.memberships.filter(
-            Q(
-                # Right directly owned by this user
-                Q(user=self.user)
-                & Q(level__gte=level)
-            )
-            | Q(
-                # Right owned by the group and by the user
-                Q(group__memberships__user=self.user)
-                & Q(level__gte=level)
-                & Q(group__memberships__level__gte=level)
-            )
-        ).exists()
+        return has_access(self.user, instance, level, skip_public=skip_public)
 
 
 class RepositoryACLMixin(ACLMixin):
diff --git a/arkindex/project/rq_overrides.py b/arkindex/project/rq_overrides.py
index 583f1f86cd..ae53b22aed 100644
--- a/arkindex/project/rq_overrides.py
+++ b/arkindex/project/rq_overrides.py
@@ -2,10 +2,10 @@ from typing import Optional
 
 from django_rq import get_connection
 from django_rq.queues import DjangoRQ
-from rq.compat import as_text, decode_redis_hash
 from rq.exceptions import NoSuchJobError
 from rq.job import Job as BaseJob
 from rq.registry import BaseRegistry
+from rq.utils import as_text, decode_redis_hash
 
 
 def as_int(value) -> Optional[int]:
diff --git a/arkindex/project/serializer_fields.py b/arkindex/project/serializer_fields.py
index 40e7830fb4..cefaaf14ec 100644
--- a/arkindex/project/serializer_fields.py
+++ b/arkindex/project/serializer_fields.py
@@ -291,3 +291,14 @@ class DatasetSetsCountField(serializers.DictField):
             .values_list("set", "count")
         )
         return elts_count
+
+
+class NullField(serializers.CharField):
+
+    def to_representation(self, value):
+        return None
+
+    def to_internal_value(self, data):
+        if data is not None:
+            self.fail("invalid")
+        return None
diff --git a/arkindex/project/settings.py b/arkindex/project/settings.py
index 0932027fe0..afa3b2206a 100644
--- a/arkindex/project/settings.py
+++ b/arkindex/project/settings.py
@@ -203,7 +203,6 @@ REST_FRAMEWORK = {
     "DEFAULT_AUTHENTICATION_CLASSES": (
         "rest_framework.authentication.SessionAuthentication",
         "rest_framework.authentication.TokenAuthentication",
-        "arkindex.ponos.authentication.AgentAuthentication",
         "arkindex.ponos.authentication.TaskAuthentication",
     ),
     "DEFAULT_PAGINATION_CLASS": "arkindex.project.pagination.PageNumberPagination",
@@ -338,21 +337,17 @@ elif conf["cache"]["type"] == CacheType.Dummy:
         }
     }
 
+_rq_queue_conf = {
+    "HOST": conf["redis"]["host"],
+    "PORT": conf["redis"]["port"],
+    "DB": conf["redis"]["db"],
+    "PASSWORD": conf["redis"]["password"],
+    "DEFAULT_TIMEOUT": conf["redis"]["timeout"],
+}
 RQ_QUEUES = {
-    "default": {
-        "HOST": conf["redis"]["host"],
-        "PORT": conf["redis"]["port"],
-        "DB": conf["redis"]["db"],
-        "PASSWORD": conf["redis"]["password"],
-        "DEFAULT_TIMEOUT": conf["redis"]["timeout"],
-    },
-    "high": {
-        "HOST": conf["redis"]["host"],
-        "PORT": conf["redis"]["port"],
-        "DB": conf["redis"]["db"],
-        "PASSWORD": conf["redis"]["password"],
-        "DEFAULT_TIMEOUT": conf["redis"]["timeout"],
-    }
+    "default": _rq_queue_conf,
+    "high": _rq_queue_conf,
+    "tasks": _rq_queue_conf,
 }
 
 RQ_TIMEOUTS = conf["job_timeouts"]
@@ -501,9 +496,10 @@ if DEBUG:
     })
 _ponos_env.update(conf["ponos"]["default_env"])
 PONOS_DEFAULT_ENV = _ponos_env
-PONOS_PRIVATE_KEY = conf["ponos"]["private_key"]
-PONOS_DEFAULT_FARM = conf["ponos"]["default_farm"]
 PONOS_ARTIFACT_MAX_SIZE = conf["ponos"]["artifact_max_size"]
+PONOS_RQ_EXECUTION = True
+# Base data directory for RQ tasks execution (in the docker container)
+PONOS_DATA_DIR = "/data"
 
 # Docker images used by our ponos workflow
 ARKINDEX_TASKS_IMAGE = conf["docker"]["tasks_image"]
@@ -581,8 +577,6 @@ if TEST_ENV:
     AWS_ACCESS_KEY = "test"
     AWS_SECRET_KEY = "test"
     AWS_ENDPOINT = "http://s3"
-    PONOS_PRIVATE_KEY = None
-    PONOS_DEFAULT_FARM = None
     PONOS_S3_ARTIFACTS_BUCKET = "ponos-artifacts"
     PONOS_S3_LOGS_BUCKET = "ponos-logs"
     LOCAL_IMAGESERVER_ID = 1
@@ -596,6 +590,9 @@ if TEST_ENV:
     warnings.filterwarnings("error", category=RuntimeWarning, module="django.core.paginator")
     warnings.filterwarnings("error", category=RuntimeWarning, module="rest_framework.pagination")
 
+    # Disable RQ tasks scheduler during tests
+    PONOS_RQ_EXECUTION = False
+
 # Optional unit tests runner with code coverage
 try:
     import xmlrunner  # noqa
diff --git a/arkindex/project/tests/config_samples/defaults.yaml b/arkindex/project/tests/config_samples/defaults.yaml
index 8945e3e4b9..bd19a0a3ef 100644
--- a/arkindex/project/tests/config_samples/defaults.yaml
+++ b/arkindex/project/tests/config_samples/defaults.yaml
@@ -61,6 +61,7 @@ job_timeouts:
   notify_process_completion: 120
   process_delete: 3600
   reindex_corpus: 7200
+  task: 36000
   worker_results_delete: 3600
 jwt_signing_key: null
 local_imageserver_id: 1
@@ -68,8 +69,6 @@ metrics_port: 3000
 ponos:
   artifact_max_size: 5368709120
   default_env: {}
-  default_farm: null
-  private_key: /somewhere/backend/arkindex/ponos.key
 public_hostname: https://default.config.arkindex.localhost
 redis:
   db: 0
diff --git a/arkindex/project/tests/config_samples/errors.yaml b/arkindex/project/tests/config_samples/errors.yaml
index ef6bb2392f..351af98d06 100644
--- a/arkindex/project/tests/config_samples/errors.yaml
+++ b/arkindex/project/tests/config_samples/errors.yaml
@@ -43,6 +43,7 @@ job_timeouts:
   move_element:
     a: b
   reindex_corpus: {}
+  task: ''
   worker_results_delete: null
 jwt_signing_key: null
 local_imageserver_id: 1
diff --git a/arkindex/project/tests/config_samples/expected_errors.yaml b/arkindex/project/tests/config_samples/expected_errors.yaml
index 8845f9c64c..0c4f96b0b2 100644
--- a/arkindex/project/tests/config_samples/expected_errors.yaml
+++ b/arkindex/project/tests/config_samples/expected_errors.yaml
@@ -24,6 +24,7 @@ job_timeouts:
   export_corpus: "int() argument must be a string, a bytes-like object or a real number, not 'list'"
   move_element: "int() argument must be a string, a bytes-like object or a real number, not 'dict'"
   reindex_corpus: "int() argument must be a string, a bytes-like object or a real number, not 'dict'"
+  task: "invalid literal for int() with base 10: ''"
   worker_results_delete: "int() argument must be a string, a bytes-like object or a real number, not 'NoneType'"
 ponos:
   artifact_max_size: cannot convert float NaN to integer
diff --git a/arkindex/project/tests/config_samples/override.yaml b/arkindex/project/tests/config_samples/override.yaml
index 3c8b35618c..3e4f69e593 100644
--- a/arkindex/project/tests/config_samples/override.yaml
+++ b/arkindex/project/tests/config_samples/override.yaml
@@ -75,7 +75,8 @@ job_timeouts:
   notify_process_completion: 6
   process_delete: 7
   reindex_corpus: 8
-  worker_results_delete: 9
+  task: 9
+  worker_results_delete: 10
 jwt_signing_key: deadbeef
 local_imageserver_id: 45
 metrics_port: 4242
@@ -83,8 +84,6 @@ ponos:
   artifact_max_size: 12345678901234567890
   default_env:
     A: B
-  default_farm: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
-  private_key: /a/b/c
 public_hostname: https://darkindex.lol
 redis:
   db: 42
diff --git a/arkindex/project/tests/openapi/test_schema.py b/arkindex/project/tests/openapi/test_schema.py
index c8404b812f..e0eb76f5c9 100644
--- a/arkindex/project/tests/openapi/test_schema.py
+++ b/arkindex/project/tests/openapi/test_schema.py
@@ -55,7 +55,6 @@ class TestAutoSchema(TestCase):
                 "security": [
                     {"cookieAuth": []},
                     {"tokenAuth": []},
-                    {"agentAuth": []},
                     {"taskAuth": []},
                     # Allows no authentication too
                     {},
diff --git a/arkindex/project/tests/test_acl_mixin.py b/arkindex/project/tests/test_acl_mixin.py
index 60856b17d7..a36975c7b2 100644
--- a/arkindex/project/tests/test_acl_mixin.py
+++ b/arkindex/project/tests/test_acl_mixin.py
@@ -1,18 +1,12 @@
 import uuid
+from unittest import expectedFailure
 
 from django.contrib.auth.models import AnonymousUser
 from django.contrib.contenttypes.models import ContentType
 
 from arkindex.documents.models import Corpus
 from arkindex.process.models import Process, ProcessMode, Repository, Revision, WorkerType
-from arkindex.project.mixins import (
-    ACLMixin,
-    CorpusACLMixin,
-    ProcessACLMixin,
-    RepositoryACLMixin,
-    TrainingModelMixin,
-    WorkerACLMixin,
-)
+from arkindex.project.mixins import ACLMixin, CorpusACLMixin, ProcessACLMixin, TrainingModelMixin
 from arkindex.project.tests import FixtureTestCase
 from arkindex.training.models import Model
 from arkindex.users.models import Group, Right, Role, User
@@ -79,6 +73,7 @@ class TestACLMixin(FixtureTestCase):
         cls.corpus_type = ContentType.objects.get_for_model(Corpus)
         cls.group_type = ContentType.objects.get_for_model(Group)
 
+    @expectedFailure
     def test_filter_right(self):
         # List all the rights with a privilege greater than 80 for user 3
         params = {
@@ -93,6 +88,7 @@ class TestACLMixin(FixtureTestCase):
             corpora = list(filter_rights(self.user3, Corpus, 80))
         self.assertCountEqual(corpora, [self.corpus1])
 
+    @expectedFailure
     def test_filter_right_including_public(self):
         # Listing readable objects involve listing public objects with a SQL Union
         params = {
@@ -109,6 +105,7 @@ class TestACLMixin(FixtureTestCase):
             [self.corpus1, self.corpus]
         )
 
+    @expectedFailure
     def test_right_direct_access(self):
         # User 2 has a direct access to the above corpus with a level of 90
         acl_mixin = ACLMixin(user=self.user2)
@@ -124,6 +121,7 @@ class TestACLMixin(FixtureTestCase):
             has_access = acl_mixin.has_access(self.corpus1, 80)
         self.assertTrue(has_access)
 
+    @expectedFailure
     def test_right_group_members_restriction(self):
         # User rights on corpora via a group are restricted to user level inside the group
         acl_mixin = ACLMixin(user=self.user1)
@@ -133,7 +131,7 @@ class TestACLMixin(FixtureTestCase):
 
     def test_corpus_acl_mixin_has_read_access(self):
         corpus_acl_mixin = CorpusACLMixin(user=self.user2)
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(0):
             read_access = corpus_acl_mixin.has_read_access(self.corpus1)
         self.assertTrue(read_access)
 
@@ -147,10 +145,11 @@ class TestACLMixin(FixtureTestCase):
     def test_corpus_acl_mixin_has_write_access(self):
         # User2 has a direct access to Corpus1 with an adequate level
         corpus_acl_mixin = CorpusACLMixin(user=self.user2)
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(0):
             write_access = corpus_acl_mixin.has_write_access(self.corpus1)
         self.assertTrue(write_access)
 
+    @expectedFailure
     def test_corpus_acl_mixin_has_admin_access(self):
         # Admin access requires either to have an admin right or to be a django admin/internal user
         admin_access = [
@@ -167,84 +166,20 @@ class TestACLMixin(FixtureTestCase):
             self.assertEqual(admin_access, access_check)
             ContentType.objects.clear_cache()
 
+    @expectedFailure
     def test_corpus_acl_mixin_writable(self):
         corpus_acl_mixin = CorpusACLMixin(user=self.user1)
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(1):
             corpora = list(corpus_acl_mixin.writable_corpora)
         self.assertCountEqual(
             list(corpora),
             [self.corpus1]
         )
 
-    def test_repo_acl_mixin_has_read_access(self):
-        repo_acl_mixin = RepositoryACLMixin(user=self.user2)
-        with self.assertNumQueries(3):
-            read_access = repo_acl_mixin.has_read_access(self.repo1)
-        self.assertTrue(read_access)
-
-    def test_repo_acl_mixin_has_write_access(self):
-        repo_acl_mixin = RepositoryACLMixin(user=self.user2)
-        with self.assertNumQueries(3):
-            exec_access = repo_acl_mixin.has_execution_access(self.repo1)
-        self.assertFalse(exec_access)
-
-    def test_repo_acl_mixin_has_admin_access(self):
-        # Admin access requires either to have an admin right or to be a django admin/internal user
-        admin_access = [
-            (self.user, 3, False),
-            (self.superuser, 0, True),
-            (self.user1, 3, False),
-            (self.user2, 3, False),
-            (self.user3, 3, False)
-        ]
-        for user, queries, access_check in admin_access:
-            repo_acl_mixin = RepositoryACLMixin(user=user)
-            with self.assertNumQueries(queries):
-                admin_access = repo_acl_mixin.has_admin_access(self.repo1)
-            self.assertEqual(admin_access, access_check)
-            ContentType.objects.clear_cache()
-
-    def test_repo_acl_mixin_readable(self):
-        repo_acl_mixin = RepositoryACLMixin(user=self.user2)
-        with self.assertNumQueries(3):
-            repos = list(repo_acl_mixin.readable_repositories)
-        self.assertCountEqual(
-            list(repos),
-            [self.repo1]
-        )
-
-    def test_repo_acl_mixin_executable(self):
-        repo_acl_mixin = RepositoryACLMixin(user=self.user2)
-        with self.assertNumQueries(3):
-            repos = list(repo_acl_mixin.executable_repositories)
-        self.assertEqual(repos, [])
-
-    def test_worker_acl_mixin_has_worker_access(self):
-        # Worker access may be determined via its repository
-
-        group1_admin = User.objects.create_user("group1+admin@test.test")
-        self.group1.memberships.create(user=group1_admin, level=Role.Admin.value)
-        access_cases = [
-            (self.superuser, 100, 0, True),
-            (self.user1, 100, 3, True),
-            (self.user, 10, 5, False),
-            (self.user2, 80, 5, False),
-            (self.user3, 10, 5, False),
-            (self.user2, 10, 5, True),
-            (group1_admin, 100, 5, False),
-            (group1_admin, 80, 5, True),
-        ]
-        for user, level, queries, access_check in access_cases:
-            worker_acl_mixin = WorkerACLMixin(user=user)
-            with self.assertNumQueries(queries):
-                access = worker_acl_mixin.has_worker_access(self.worker, level)
-            self.assertEqual(access, access_check)
-            ContentType.objects.clear_cache()
-
     def test_corpus_readable_orderable(self):
         # Assert corpora retrieved via the mixin are still orderable
         corpus_acl_mixin = CorpusACLMixin(user=self.user3)
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(1):
             corpora = list(corpus_acl_mixin.readable_corpora.order_by("name"))
         self.assertListEqual(
             [c.name for c in corpora],
@@ -261,6 +196,7 @@ class TestACLMixin(FixtureTestCase):
             list(Corpus.objects.all())
         )
 
+    @expectedFailure
     def test_anonymous_user_readable_corpora(self):
         # An anonymous user should have guest access to any public corpora
         corpus_acl_mixin = CorpusACLMixin(user=AnonymousUser())
@@ -275,33 +211,39 @@ class TestACLMixin(FixtureTestCase):
         # User specific rights should be returned instead of the the defaults access for public rights
         Right.objects.create(user=self.user3, content_object=self.corpus, level=42)
         corpus_acl_mixin = CorpusACLMixin(user=self.user3)
-        with self.assertNumQueries(2):
+        with self.assertNumQueries(1):
             corpora = list(corpus_acl_mixin.readable_corpora)
         self.assertCountEqual(
             list(corpora),
             [self.corpus1, self.corpus2, self.corpus]
         )
 
+    @expectedFailure
     def test_max_level_does_not_exists(self):
         with self.assertNumQueries(3):
             self.assertEqual(get_max_level(self.user1, self.corpus2), None)
 
+    @expectedFailure
     def test_max_level_user1(self):
         with self.assertNumQueries(3):
             self.assertEqual(get_max_level(self.user1, self.repo1), 80)
 
+    @expectedFailure
     def test_max_level_user2(self):
         with self.assertNumQueries(3):
             self.assertEqual(get_max_level(self.user2, self.repo1), 10)
 
+    @expectedFailure
     def test_max_level_user3(self):
         with self.assertNumQueries(3):
             self.assertEqual(get_max_level(self.user3, self.corpus2), 75)
 
+    @expectedFailure
     def test_max_level_public(self):
         with self.assertNumQueries(3):
             self.assertEqual(get_max_level(self.user1, self.corpus), Role.Guest.value)
 
+    @expectedFailure
     def test_process_access_project(self):
         """
         A project attached to a process defines its access rights
@@ -310,18 +252,7 @@ class TestACLMixin(FixtureTestCase):
         with self.assertNumQueries(3):
             self.assertEqual(ProcessACLMixin(user=self.user1).process_access_level(process), 75)
 
-    def test_process_access_docker_build(self):
-        """
-        Access to a docker build process is possible for someone with an access to the origin repository
-        """
-        process = Process.objects.create(
-            mode=ProcessMode.Repository,
-            creator=self.user2,
-            revision=self.repo1.revisions.create(hash="42", message="Message", author="User 1")
-        )
-        with self.assertNumQueries(3):
-            self.assertEqual(ProcessACLMixin(user=self.user1).process_access_level(process), 80)
-
+    @expectedFailure
     def test_process_access_list(self):
         """
         A user can list process if they have rights on its corresponding project or repository
@@ -345,7 +276,7 @@ class TestACLMixin(FixtureTestCase):
             revision=Revision.objects.get(message="My w0rk3r")
         )
 
-        with self.assertNumQueries(4):
+        with self.assertNumQueries(1):
             readable_process_ids = list(
                 ProcessACLMixin(user=self.user1).readable_processes.values_list("id", flat=True)
             )
@@ -358,15 +289,16 @@ class TestACLMixin(FixtureTestCase):
         """
         User5 has guest access to Model1 via Group3
         """
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(0):
             read_access = TrainingModelMixin(user=self.user5).has_read_access(self.model1)
         self.assertTrue(read_access)
 
+    @expectedFailure
     def test_models_no_access_user(self):
         """
         User3 has no guest access to Model1
         """
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(0):
             read_access = TrainingModelMixin(user=self.user3).has_read_access(self.model1)
         self.assertFalse(read_access)
 
@@ -374,7 +306,7 @@ class TestACLMixin(FixtureTestCase):
         """
         User4 has contributor access to Model1
         """
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(0):
             write_access = TrainingModelMixin(user=self.user4).has_write_access(self.model1)
         self.assertTrue(write_access)
 
@@ -382,23 +314,25 @@ class TestACLMixin(FixtureTestCase):
         """
         User5 has admin access to Model2 via Group3's access rights
         """
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(0):
             admin_access = TrainingModelMixin(user=self.user5).has_admin_access(self.model2)
         self.assertTrue(admin_access)
 
+    @expectedFailure
     def test_models_readable(self):
         """
         To view a model, a user needs guest access.
         """
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(1):
             readable_models = list(TrainingModelMixin(user=self.user4).readable_models)
         self.assertListEqual(readable_models, [self.model1])
 
+    @expectedFailure
     def test_models_editable(self):
         """
         To edit a model, a user needs contributor access.
         User5 only has that access on model2.
         """
-        with self.assertNumQueries(3):
+        with self.assertNumQueries(1):
             editable_models = list(TrainingModelMixin(user=self.user5).editable_models)
         self.assertListEqual(editable_models, [self.model2])
diff --git a/arkindex/project/tools.py b/arkindex/project/tools.py
index 6368ef0f6f..72fde5b89d 100644
--- a/arkindex/project/tools.py
+++ b/arkindex/project/tools.py
@@ -1,9 +1,6 @@
 from collections.abc import Iterable, Iterator, Sized
 from datetime import datetime, timezone
 
-from cryptography.hazmat.backends import default_backend
-from cryptography.hazmat.primitives.asymmetric import ec
-from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
 from django.db.models import Aggregate, CharField, Func
 from django.db.models.expressions import BaseExpression, OrderByList
 from django.urls import reverse
@@ -186,24 +183,6 @@ class PercentileCont(OrderedSetAggregate):
     function = "percentile_cont"
 
 
-def build_public_key() -> str:
-    """
-    Quickly build a Ponos public key as a PEM-encoded string
-    """
-    return (
-        ec.generate_private_key(
-            ec.SECP384R1(),
-            default_backend(),
-        )
-        .public_key()
-        .public_bytes(
-            Encoding.PEM,
-            PublicFormat.SubjectPublicKeyInfo,
-        )
-        .decode("utf-8")
-    )
-
-
 def fake_now():
     """
     Fake creation date for fixtures and test objects
diff --git a/arkindex/project/triggers.py b/arkindex/project/triggers.py
index 46eee4a05a..6512fc7b55 100644
--- a/arkindex/project/triggers.py
+++ b/arkindex/project/triggers.py
@@ -5,6 +5,7 @@ from typing import Literal, Optional, Union
 from uuid import UUID
 
 from django.db.models import Prefetch, prefetch_related_objects
+from rq.job import Dependency
 
 from arkindex.documents import export
 from arkindex.documents import tasks as documents_tasks
@@ -230,3 +231,17 @@ def notify_process_completion(process: Process):
             process=process,
             subject=f"Your process {process_name} finished {state_msg[state]}",
         )
+
+
+def schedule_tasks(process: Process, run: int):
+    """Run tasks of a process in RQ, one by one"""
+    tasks = process.tasks.using("default").filter(run=run).order_by("depth", "id")
+    # Initially mark all tasks as pending
+    tasks.update(state=State.Pending)
+    # Build a simple dependency scheme between tasks, based on depth
+    parent_job = None
+    for task in tasks:
+        kwargs = {}
+        if parent_job:
+            kwargs["depends_on"] = Dependency(jobs=[parent_job], allow_failure=True)
+        parent_job = ponos_tasks.run_task_rq.delay(task, **kwargs)
diff --git a/arkindex/sql_validation/corpus_delete.sql b/arkindex/sql_validation/corpus_delete.sql
index f15fa91791..a454d05cdb 100644
--- a/arkindex/sql_validation/corpus_delete.sql
+++ b/arkindex/sql_validation/corpus_delete.sql
@@ -158,7 +158,7 @@ WHERE "documents_selection"."id" IN
 DELETE
 FROM "users_right"
 WHERE ("users_right"."content_id" = '{corpus_id}'::uuid
-       AND "users_right"."content_type_id" = 20);
+       AND "users_right"."content_type_id" = 19);
 
 DELETE
 FROM "documents_corpusexport"
diff --git a/arkindex/sql_validation/corpus_delete_top_level_type.sql b/arkindex/sql_validation/corpus_delete_top_level_type.sql
index 7acf28db77..9c7ae8cd60 100644
--- a/arkindex/sql_validation/corpus_delete_top_level_type.sql
+++ b/arkindex/sql_validation/corpus_delete_top_level_type.sql
@@ -162,7 +162,7 @@ WHERE "documents_selection"."id" IN
 DELETE
 FROM "users_right"
 WHERE ("users_right"."content_id" = '{corpus_id}'::uuid
-       AND "users_right"."content_type_id" = 20);
+       AND "users_right"."content_type_id" = 19);
 
 DELETE
 FROM "documents_corpusexport"
diff --git a/arkindex/training/api.py b/arkindex/training/api.py
index c8d6ce5830..55acaf6f0d 100644
--- a/arkindex/training/api.py
+++ b/arkindex/training/api.py
@@ -392,7 +392,7 @@ class ModelsList(TrainingModelMixin, ListCreateAPIView):
             filters &= Q(archived__isnull=self.request.query_params["archived"].lower().strip() in ("false", "0"))
 
         # Use the default database to prevent a stale read when a model has just been created
-        return self.readable_models.using("default").filter(filters).order_by("name")
+        return Model.objects.readable(self.request.user).using("default").filter(filters).order_by("name")
 
     def perform_create(self, serializer):
         model_name = serializer.validated_data.get("name")
@@ -443,7 +443,7 @@ class ModelRetrieve(TrainingModelMixin, RetrieveUpdateAPIView):
     serializer_class = ModelSerializer
 
     def get_queryset(self):
-        return self.readable_models
+        return Model.objects.readable(self.request.user)
 
     def check_object_permissions(self, request, obj):
         super().check_object_permissions(request, obj)
diff --git a/arkindex/training/tests/test_datasets_api.py b/arkindex/training/tests/test_datasets_api.py
index f6a935b162..a97ad190a6 100644
--- a/arkindex/training/tests/test_datasets_api.py
+++ b/arkindex/training/tests/test_datasets_api.py
@@ -1,5 +1,5 @@
 import uuid
-from unittest.mock import MagicMock, patch
+from unittest.mock import MagicMock, call, patch
 
 from django.urls import reverse
 from django.utils import timezone as DjangoTimeZone
@@ -61,13 +61,18 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertDictEqual(response.json(), {"detail": "Not found."})
 
-    def test_list_private_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_list_private_corpus(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:corpus-datasets", kwargs={"pk": self.private_corpus.pk}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have guest access to this corpus."})
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Guest.value, skip_public=False))
+
     def test_list(self):
         self.client.force_login(self.user)
         with self.assertNumQueries(5):
@@ -137,20 +142,25 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
         self.assertDictEqual(response.json(), {"detail": "Not found."})
 
-    def test_create_private_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_private_corpus(self, has_access_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.private_corpus.pk}),
                 data={"name": "My dataset", "description": "My dataset for my experiments."},
                 format="json"
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have contributor access to this corpus."})
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.private_corpus, Role.Contributor.value, skip_public=False))
+
     def test_create_name_required(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"description": "My dataset for my experiments."},
@@ -161,7 +171,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_name_empty(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "", "description": "My dataset for my experiments."},
@@ -172,7 +182,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_name_blank(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "        ", "description": "My dataset for my experiments."},
@@ -183,7 +193,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_name_too_long(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={
@@ -197,7 +207,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_name_already_exists_in_corpus(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={
@@ -211,7 +221,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_description_required(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset"},
@@ -222,7 +232,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_description_empty(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset", "description": ""},
@@ -233,7 +243,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_description_blank(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset", "description": "             "},
@@ -244,7 +254,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset", "description": "My dataset for my experiments."},
@@ -272,7 +282,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_state_ignored(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset", "description": "My dataset for my experiments.", "state": "complete"},
@@ -297,7 +307,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_sets(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset", "description": "My dataset for my experiments.", "sets": ["a", "b", "c", "d"]},
@@ -321,7 +331,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_sets_length(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset", "description": "My dataset for my experiments.", "sets": []},
@@ -332,7 +342,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_sets_unique_names(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset", "description": "My dataset for my experiments.", "sets": ["a", "a", "b"]},
@@ -343,7 +353,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_sets_blank_names(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={"name": "My dataset", "description": "My dataset for my experiments.", "sets": ["     ", " ", "b"]},
@@ -360,7 +370,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_create_sets_name_too_long(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:corpus-datasets", kwargs={"pk": self.corpus.pk}),
                 data={
@@ -409,9 +419,10 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_update_requires_write_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_update_requires_write_corpus(self, has_access_mock):
         self.client.force_login(self.read_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -424,9 +435,12 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have contributor access to corpus Unit Tests."})
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.read_user, self.corpus, Role.Contributor.value, skip_public=False))
+
     def test_update_doesnt_exist(self):
         self.client.force_login(self.read_user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}),
                 data={
@@ -441,7 +455,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_update_name_too_long(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -456,7 +470,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
     def test_update_name_already_exists_in_corpus(self):
         Dataset.objects.create(name="Another Dataset", description="A set of data", corpus=self.corpus, creator=self.dataset_creator)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -471,7 +485,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_update_requires_all_fields(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={"name": "Shin Seiki Evangelion"},
@@ -483,7 +497,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
     def test_update_add_sets(self):
         self.client.force_login(self.user)
         self.assertIsNone(self.dataset.task_id)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -509,7 +523,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         ProcessDataset.objects.get(process=self.process, dataset=self.dataset).delete()
         self.client.force_login(self.user)
         dataset_elt = self.dataset.dataset_elements.create(element=self.page1, set="training")
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -542,7 +556,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.dataset.dataset_elements.create(element_id=self.page1.id, set="training")
         self.dataset.dataset_elements.create(element_id=self.page2.id, set="validation")
         self.dataset.dataset_elements.create(element_id=self.page3.id, set="validation")
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -577,7 +591,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.dataset.dataset_elements.create(element_id=self.page1.id, set="training")
         self.dataset.dataset_elements.create(element_id=self.page2.id, set="validation")
         self.dataset.dataset_elements.create(element_id=self.page3.id, set="validation")
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -612,7 +626,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         # Remove ProcessDataset relation
         ProcessDataset.objects.get(process=self.process, dataset=self.dataset).delete()
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -627,7 +641,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_update_sets_length(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -642,7 +656,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_update_sets_unique_names(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -657,7 +671,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_update_sets_name_too_long(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -676,7 +690,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_update_empty_or_blank_description_or_name(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -695,7 +709,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_update_all_errors(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -716,7 +730,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.dataset.state = DatasetState.Building
         self.dataset.save()
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -734,7 +748,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
     def test_update_ponos_task_state_update(self):
         self.dataset.state = DatasetState.Building
         self.dataset.save()
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
@@ -799,7 +813,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         for new_state in DatasetState:
             with self.subTest(new_state=new_state):
-                with self.assertNumQueries(6):
+                with self.assertNumQueries(3):
                     response = self.client.put(
                         reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                         data={
@@ -818,7 +832,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_update_ponos_task_state_requires_dataset_in_process(self):
         self.process.process_datasets.all().delete()
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
@@ -835,7 +849,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         })
 
     def test_update_ponos_task_bad_state(self):
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
@@ -875,9 +889,10 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_partial_update_requires_write_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_partial_update_requires_write_corpus(self, has_access_mock):
         self.client.force_login(self.read_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={"name": "Shin Seiki Evangelion"},
@@ -886,9 +901,12 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have contributor access to corpus Unit Tests."})
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.read_user, self.corpus, Role.Contributor.value, skip_public=False))
+
     def test_partial_update_name_too_long(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -902,7 +920,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
     def test_partial_update_name_already_exists_in_corpus(self):
         Dataset.objects.create(name="Another Dataset", description="A set of data", corpus=self.corpus, creator=self.dataset_creator)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -915,7 +933,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_partial_update(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -932,7 +950,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_partial_update_empty_or_blank_description_or_name(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -946,7 +964,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_partial_update_requires_ponos_auth(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -960,7 +978,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         })
 
     def test_partial_update_ponos_task_state_update(self):
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
@@ -975,7 +993,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_partial_update_ponos_task_state_requires_dataset_in_process(self):
         self.process.process_datasets.all().delete()
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
@@ -990,7 +1008,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         })
 
     def test_partial_update_ponos_task_bad_state(self):
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 HTTP_AUTHORIZATION=f"Ponos {self.task.token}",
@@ -1006,7 +1024,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_partial_update_sets_name_too_long(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -1069,7 +1087,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         for new_state in DatasetState:
             with self.subTest(new_state=new_state):
-                with self.assertNumQueries(6):
+                with self.assertNumQueries(3):
                     response = self.client.patch(
                         reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                         data={
@@ -1106,18 +1124,24 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_retrieve_requires_read_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_retrieve_requires_read_corpus(self, filter_rights_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+
+        with self.assertNumQueries(2):
             response = self.client.get(
                 reverse("api:dataset-update", kwargs={"pk": self.private_dataset.pk})
             )
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
         self.assertDictEqual(response.json(), {"detail": "Not found."})
 
-    def test_retrieve_doesnt_exists(self):
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
+    def test_retrieve_doesnt_exist(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(
                 reverse("api:dataset-update", kwargs={"pk": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"})
             )
@@ -1126,7 +1150,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_retrieve(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk})
             )
@@ -1149,7 +1173,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.dataset.task = self.task
         self.dataset.save()
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk})
             )
@@ -1176,18 +1200,22 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_delete_requires_write_corpus(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_delete_requires_corpus_admin(self, has_access_mock):
         self.client.force_login(self.write_user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have admin access to corpus Unit Tests."})
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.write_user, self.corpus, Role.Admin.value, skip_public=False))
+
     def test_delete_doesnt_exist(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:dataset-update", kwargs={"pk": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"}),
             )
@@ -1196,7 +1224,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_delete(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
             )
@@ -1211,7 +1239,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
             with self.subTest(state=state):
                 self.dataset.state = state
                 self.dataset.save()
-                with self.assertNumQueries(5):
+                with self.assertNumQueries(3):
                     response = self.client.delete(
                         reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                     )
@@ -1229,7 +1257,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.dataset.dataset_elements.create(element_id=self.page3.id, set="validation")
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
             )
@@ -1253,20 +1281,24 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_list_elements_invalid_dataset_id(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:dataset-elements", kwargs={"pk": str(uuid.uuid4())}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_list_elements_readable_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_list_elements_readable_corpus(self, filter_rights_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.get(reverse("api:dataset-elements", kwargs={"pk": str(self.private_dataset.id)}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
     def test_list_elements_set_filter_wrong_set(self):
         self.dataset.dataset_elements.create(element_id=self.page1.id, set="test")
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.get(
                 reverse("api:dataset-elements", kwargs={"pk": str(self.dataset.id)}),
                 data={"set": "aaaaa"}
@@ -1283,7 +1315,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.dataset.dataset_elements.create(element_id=self.page1.id, set="test")
         self.dataset.dataset_elements.create(element_id=self.page2.id, set="training")
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(
                 reverse("api:dataset-elements", kwargs={"pk": self.dataset.pk}),
                 data={"set": "training", "with_count": "true"},
@@ -1491,7 +1523,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.dataset.state = state
             self.dataset.save()
             with self.subTest(state=state):
-                with self.assertNumQueries(6):
+                with self.assertNumQueries(4):
                     response = self.client.get(
                         reverse("api:dataset-elements", kwargs={"pk": self.dataset.pk}),
                         {"page_size": 3},
@@ -1516,18 +1548,23 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_add_element_private_dataset(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_add_element_private_dataset(self, filter_rights_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.post(
                 reverse("api:dataset-elements", kwargs={"pk": self.private_dataset.id})
             )
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_add_element_requires_writable_corpus(self):
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_add_element_requires_writable_corpus(self, has_access_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:dataset-elements", kwargs={"pk": self.dataset.id})
             )
@@ -1536,9 +1573,12 @@ class TestDatasetsAPI(FixtureAPITestCase):
             "detail": "You do not have contributor access to the corpus of this dataset."
         })
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.corpus, Role.Contributor.value, skip_public=False))
+
     def test_add_element_required_fields(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:dataset-elements", kwargs={"pk": self.dataset.id}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -1549,7 +1589,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
     def test_add_element_wrong_element(self):
         element = self.private_corpus.elements.create(type=self.private_corpus.types.create(slug="folder"))
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:dataset-elements", kwargs={"pk": self.dataset.id}),
                 data={"set": "test", "element_id": str(element.id)},
@@ -1562,7 +1602,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_add_element_wrong_set(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:dataset-elements", kwargs={"pk": self.dataset.id}),
                 data={"set": "aaaaaaaaaaa", "element_id": str(self.vol.id)},
@@ -1577,7 +1617,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.dataset.state = DatasetState.Complete
         self.dataset.save()
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:dataset-elements", kwargs={"pk": self.dataset.id}),
                 data={"set": "test", "element_id": str(self.vol.id)},
@@ -1589,7 +1629,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
     def test_add_element_already_exists(self):
         self.dataset.dataset_elements.create(element=self.page1, set="test")
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:dataset-elements", kwargs={"pk": self.dataset.id}),
                 data={"set": "test", "element_id": str(self.page1.id)},
@@ -1600,7 +1640,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_add_element(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(13):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:dataset-elements", kwargs={"pk": self.dataset.id}),
                 data={"set": "training", "element_id": str(self.page1.id)},
@@ -1636,18 +1676,23 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_add_from_selection_private_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_add_from_selection_private_corpus(self, filter_rights_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.post(
                 reverse("api:dataset-elements-selection", kwargs={"pk": self.private_corpus.id})
             )
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_add_from_selection_requires_writable_corpus(self):
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_add_from_selection_requires_writable_corpus(self, has_access_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:dataset-elements-selection", kwargs={"pk": self.corpus.id})
             )
@@ -1656,9 +1701,12 @@ class TestDatasetsAPI(FixtureAPITestCase):
             "detail": "You need a Contributor access to the corpus to perform this action."
         })
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.corpus, Role.Contributor.value, skip_public=False))
+
     def test_add_from_selection_required_fields(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:dataset-elements-selection", kwargs={"pk": self.corpus.id}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {
@@ -1668,7 +1716,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_add_from_selection_wrong_values(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:dataset-elements-selection", kwargs={"pk": self.corpus.id}),
                 data={"set": {}, "dataset_id": "AAA"},
@@ -1682,7 +1730,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_add_from_selection_wrong_dataset(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:dataset-elements-selection", kwargs={"pk": self.corpus.id}),
                 data={"set": "aaa", "dataset_id": self.private_dataset.id},
@@ -1698,7 +1746,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.dataset.state = DatasetState.Complete
         self.dataset.save()
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:dataset-elements-selection", kwargs={"pk": self.corpus.id}),
                 data={"set": "aaa", "dataset_id": self.dataset.id},
@@ -1711,7 +1759,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
 
     def test_add_from_selection_wrong_set(self):
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:dataset-elements-selection", kwargs={"pk": self.corpus.id}),
                 data={"set": "aaa", "dataset_id": self.dataset.id},
@@ -1731,7 +1779,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.user.selected_elements.set([self.vol, self.page1, self.page2])
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:dataset-elements-selection", kwargs={"pk": self.corpus.id}),
                 data={"set": "training", "dataset_id": self.dataset.id},
@@ -1747,15 +1795,17 @@ class TestDatasetsAPI(FixtureAPITestCase):
             ]
         )
 
-    # ListElementDatasets
-
-    def test_element_datasets_requires_read_access(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_element_datasets_requires_read_access(self, filter_rights_mock):
         self.client.force_login(self.user)
         private_elt = self.private_corpus.elements.create(type=self.private_corpus.types.create(slug="t"), name="elt")
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(2):
             response = self.client.get(reverse("api:element-datasets", kwargs={"pk": private_elt.id}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
     def test_element_datasets_methods(self):
         self.client.force_login(self.user)
         forbidden_methods = ("post", "patch", "put", "delete")
@@ -1803,7 +1853,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.dataset.dataset_elements.create(element=self.page1, set="train")
         self.dataset.dataset_elements.create(element=self.page1, set="validation")
         self.dataset2.dataset_elements.create(element=self.page1, set="train")
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:element-datasets", kwargs={"pk": str(self.page1.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertDictEqual(response.json(), {
@@ -1870,7 +1920,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.dataset.dataset_elements.create(element=self.page1, set="train")
         self.dataset.dataset_elements.create(element=self.page1, set="validation")
         self.dataset2.dataset_elements.create(element=self.page1, set="train")
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:element-datasets", kwargs={"pk": str(self.page1.id)}), {"with_neighbors": False})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertDictEqual(response.json(), {
@@ -1947,7 +1997,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         sorted_dataset2_elements = sorted([str(self.page1.id), str(self.page3.id)])
         page1_index_2 = sorted_dataset2_elements.index(str(self.page1.id))
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(7):
             response = self.client.get(reverse("api:element-datasets", kwargs={"pk": str(self.page1.id)}), {"with_neighbors": True})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertDictEqual(response.json(), {
@@ -2014,12 +2064,12 @@ class TestDatasetsAPI(FixtureAPITestCase):
                 "set": "train",
                 "previous": (
                     sorted_dataset2_elements[page1_index_2 - 1]
-                    if page1_index_1 - 1 >= 0
+                    if page1_index_2 - 1 >= 0
                     else None
                 ),
                 "next": (
                     sorted_dataset2_elements[page1_index_2 + 1]
-                    if page1_index_1 + 1 <= 1
+                    if page1_index_2 + 1 <= 2
                     else None
                 )
             }]
@@ -2051,26 +2101,36 @@ class TestDatasetsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_clone_private_corpus(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_clone_private_corpus(self, filter_rights_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:dataset-clone", kwargs={"pk": self.private_dataset.id})
             )
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_clone_requires_writable_corpus(self):
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user, Corpus, Role.Guest.value))
+
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_clone_requires_writable_corpus(self, has_access_mock):
         self.corpus.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
-        with self.assertNumQueries(9):
+
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:dataset-clone", kwargs={"pk": self.dataset.id})
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {
             "detail": "You need a Contributor access to the dataset's corpus."
         })
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user, self.corpus, Role.Contributor.value, skip_public=False))
+
     def test_clone(self):
         self.dataset.creator = self.superuser
         self.dataset.state = DatasetState.Error
@@ -2082,7 +2142,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.assertCountEqual(self.corpus.datasets.values_list("name", flat=True), ["First Dataset", "Second Dataset"])
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(12):
             response = self.client.post(
                 reverse("api:dataset-clone", kwargs={"pk": self.dataset.id}),
                 format="json",
@@ -2124,7 +2184,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
     def test_clone_existing_name(self):
         self.corpus.datasets.create(name="Clone of First Dataset", creator=self.user)
         self.client.force_login(self.user)
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:dataset-clone", kwargs={"pk": self.dataset.id}),
                 format="json",
@@ -2158,7 +2218,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
     def test_clone_name_too_long(self):
         dataset = self.corpus.datasets.create(name="A" * 99, creator=self.user)
         self.client.force_login(self.user)
-        with self.assertNumQueries(14):
+        with self.assertNumQueries(11):
             response = self.client.post(
                 reverse("api:dataset-clone", kwargs={"pk": dataset.id}),
                 format="json",
@@ -2191,13 +2251,14 @@ class TestDatasetsAPI(FixtureAPITestCase):
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_destroy_dataset_element_requires_contributor(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_destroy_dataset_element_requires_contributor(self, has_access_mock):
         self.client.force_login(self.read_user)
         self.dataset.dataset_elements.create(element=self.page1, set="train")
         self.dataset.dataset_elements.create(element=self.page1, set="validation")
         self.assertEqual(self.dataset.dataset_elements.filter(set="train").count(), 1)
         self.assertEqual(self.dataset.dataset_elements.filter(set="validation").count(), 1)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse(
                 "api:dataset-element",
                 kwargs={"dataset": str(self.dataset.id), "element": str(self.page1.id)})
@@ -2209,6 +2270,9 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.assertEqual(self.dataset.dataset_elements.filter(set="train").count(), 1)
         self.assertEqual(self.dataset.dataset_elements.filter(set="validation").count(), 1)
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.read_user, self.corpus, Role.Contributor.value, skip_public=False))
+
     def test_destroy_dataset_element_set_required(self):
         self.client.force_login(self.user)
         with self.assertNumQueries(2):
@@ -2303,7 +2367,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         self.dataset.dataset_elements.create(element=self.page1, set="validation")
         self.assertEqual(self.dataset.dataset_elements.filter(set="train").count(), 1)
         self.assertEqual(self.dataset.dataset_elements.filter(set="validation").count(), 1)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.delete(reverse(
                 "api:dataset-element",
                 kwargs={"dataset": str(self.dataset.id), "element": str(self.page1.id)})
diff --git a/arkindex/training/tests/test_metrics_api.py b/arkindex/training/tests/test_metrics_api.py
index 4876b9904b..a1efc6de3c 100644
--- a/arkindex/training/tests/test_metrics_api.py
+++ b/arkindex/training/tests/test_metrics_api.py
@@ -1,5 +1,5 @@
 from datetime import datetime, timezone
-from unittest.mock import patch
+from unittest.mock import call, patch
 
 from django.db import IntegrityError
 from django.urls import reverse
@@ -73,10 +73,10 @@ class TestMetricsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"model_version_id": ['Invalid pk "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - object does not exist.']})
 
-    def test_create_metric_value_requires_contributor(self):
-        self.user.rights.all().delete()
+    @patch("arkindex.training.serializers.get_max_level", return_value=None)
+    def test_create_metric_value_requires_contributor(self, get_max_level_mock):
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -89,12 +89,15 @@ class TestMetricsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have contributor access to this model."})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.model))
+
     def test_create_metric_value_archived(self):
         self.model.archived = datetime.now(timezone.utc)
         self.model.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -118,7 +121,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         self.assertEqual(len(MetricKey.objects.filter(model_version=self.model_version)), 1)
         self.assertEqual(len(MetricValue.objects.filter(metric=self.metric_key)), 0)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -147,7 +150,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         with self.assertRaises(MetricKey.DoesNotExist):
             MetricKey.objects.get(name="my_metric")
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -176,7 +179,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         self.client.force_login(self.user)
         with self.assertRaises(MetricKey.DoesNotExist):
             MetricKey.objects.get(name="my_metric")
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -206,7 +209,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         Cannot create a metric value of type point using the step property
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(7):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -226,7 +229,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         Cannot create a metric value using the step property for a metric key of type point
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -245,7 +248,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         Cannot create a metric value for an existing metric key with a different mode
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -266,7 +269,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         MetricValue.objects.create(metric=self.metric_key, value=2.3)
         self.assertEqual(MetricKey.objects.get(name="a test metric").values.count(), 1)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -296,7 +299,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         metric_key = MetricKey.objects.create(name="a series metric", mode=MetricMode.Series, model_version=self.model_version)
         MetricValue.objects.create(metric=metric_key, value=3, step=1)
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -324,7 +327,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         metric_key = MetricKey.objects.create(name="a series metric", mode=MetricMode.Series, model_version=self.model_version)
         MetricValue.objects.create(metric=metric_key, value=3, step=1)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -348,7 +351,7 @@ class TestMetricsAPI(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -371,7 +374,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         self.assertEqual(len(MetricValue.objects.filter(metric=series_metric)), 1)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metric-create"),
                 data={
@@ -435,10 +438,11 @@ class TestMetricsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"model_version_id": ['Invalid pk "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - object does not exist.']})
 
-    def test_bulk_create_metric_value_requires_contributor(self):
+    @patch("arkindex.training.serializers.get_max_level", return_value=None)
+    def test_bulk_create_metric_value_requires_contributor(self, get_max_level_mock):
         self.user.rights.all().delete()
         self.client.force_login(self.user)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -460,12 +464,15 @@ class TestMetricsAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have contributor access to this model."})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.model))
+
     def test_bulk_create_metric_value_archived(self):
         self.model.archived = datetime.now(timezone.utc)
         self.model.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -493,7 +500,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         Cannot send two metrics values for the same metric key / name
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -525,7 +532,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         test_metric = MetricKey.objects.create(name="metric numero duo", model_version=self.model_version, mode=MetricMode.Series)
         self.client.force_login(self.user)
         self.assertEqual(len(MetricKey.objects.filter(model_version=self.model_version)), 2)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -569,7 +576,7 @@ class TestMetricsAPI(FixtureAPITestCase):
             MetricKey.objects.get(name="some metric")
         with self.assertRaises(MetricKey.DoesNotExist):
             MetricKey.objects.get(name="another metric")
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -621,7 +628,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         test_metric_2 = MetricKey.objects.create(name="metric number three", model_version=self.model_version, mode=MetricMode.Series)
         datetime_mock.return_value = datetime(2046, 1, 1, 12, 34, 56, tzinfo=timezone.utc)
         self.client.force_login(self.user)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -661,7 +668,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         MetricKey.objects.create(name="metric numero duo", model_version=self.model_version, mode=MetricMode.Point)
         MetricKey.objects.create(name="another metric", model_version=self.model_version, mode=MetricMode.Series)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -703,7 +710,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         MetricValue.objects.create(metric=self.metric_key, value=2.3)
         self.assertEqual(MetricKey.objects.get(name="a test metric").values.count(), 1)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -730,7 +737,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         Cannot create a metric value for an existing metric key with a different mode (bulk edition)
         """
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -756,7 +763,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         MetricValue.objects.create(metric=series_metric, step=1, value=0.2)
         self.assertEqual(len(MetricValue.objects.filter(metric=series_metric)), 1)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -783,7 +790,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         MetricKey.objects.create(name="another metric", model_version=self.model_version, mode=MetricMode.Series)
         MetricValue.objects.create(metric=metric_duo, value=56)
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -844,7 +851,7 @@ class TestMetricsAPI(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
@@ -871,7 +878,7 @@ class TestMetricsAPI(FixtureAPITestCase):
         self.assertEqual(len(MetricValue.objects.filter(metric=series_metric)), 1)
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:metrics-create"),
                 data={
diff --git a/arkindex/training/tests/test_model_api.py b/arkindex/training/tests/test_model_api.py
index b69071b158..7a264b2f3f 100644
--- a/arkindex/training/tests/test_model_api.py
+++ b/arkindex/training/tests/test_model_api.py
@@ -1,4 +1,4 @@
-from unittest.mock import patch
+from unittest.mock import call, patch
 from uuid import uuid4
 
 from django.urls import reverse
@@ -94,6 +94,7 @@ class TestModelAPI(FixtureAPITestCase):
     def test_create_model_version_requires_verified(self):
         user = User.objects.create(email="not_verified@mail.com", display_name="Not Verified", verified_email=False)
         self.client.force_login(user)
+
         with self.assertNumQueries(2):
             response = self.client.post(
                 reverse("api:model-versions", kwargs={"pk": str(self.model2.id)}),
@@ -104,25 +105,31 @@ class TestModelAPI(FixtureAPITestCase):
                 },
                 format="json",
             )
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_create_model_version_requires_contributor(self):
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Guest.value)
+    def test_create_model_version_requires_contributor(self, get_max_level_mock):
         """
         Can't create model version as guest
         """
         self.client.force_login(self.user1)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:model-versions", kwargs={"pk": str(self.model2.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You need a Contributor access to the model to create a new version."})
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user1, self.model2))
 
     def test_create_model_version_archived(self):
         self.model1.archived = timezone.now()
         self.model1.save()
         self.client.force_login(self.user1)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:model-versions", kwargs={"pk": str(self.model1.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -140,7 +147,7 @@ class TestModelAPI(FixtureAPITestCase):
         # To mock the creation date
         with patch("django.utils.timezone.now") as mock_now:
             mock_now.return_value = fake_now
-            with self.assertNumQueries(7):
+            with self.assertNumQueries(4):
                 response = self.client.post(
                     reverse("api:model-versions", kwargs={"pk": str(self.model1.id)}),
                     {},
@@ -175,7 +182,7 @@ class TestModelAPI(FixtureAPITestCase):
         Raise 400 when creating a model version with a blank tag
         """
         self.client.force_login(self.user1)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:model-versions", kwargs={"pk": str(self.model1.id)}),
                 {"tag": ""},
@@ -190,7 +197,7 @@ class TestModelAPI(FixtureAPITestCase):
         Raise 400 when creating a model version with an existing tag for the same model
         """
         self.client.force_login(self.user1)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:model-versions", kwargs={"pk": str(self.model1.id)}),
                 {"tag": "tagged"},
@@ -211,7 +218,7 @@ class TestModelAPI(FixtureAPITestCase):
         # To mock the creation date
         with patch("django.utils.timezone.now") as mock_now:
             mock_now.return_value = fake_now
-            with self.assertNumQueries(8):
+            with self.assertNumQueries(5):
                 response = self.client.post(
                     reverse("api:model-versions", kwargs={"pk": str(self.model1.id)}),
                     {
@@ -249,7 +256,7 @@ class TestModelAPI(FixtureAPITestCase):
         Raises 400 when creating a model version that already exists, same model_id and tag
         """
         self.client.force_login(self.user1)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:model-versions", kwargs={"pk": str(self.model1.id)}),
                 {"tag": self.model_version2.tag},
@@ -273,41 +280,21 @@ class TestModelAPI(FixtureAPITestCase):
             response = self.client.get(reverse("api:model-retrieve", kwargs={"pk": str(self.model2.id)}))
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_retrieve_model_requires_guest(self):
-        self.assertFalse(self.model1.public)
-        self.assertFalse(self.model1.memberships.filter(user=self.user3).exists())
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Model.objects.none())
+    def test_retrieve_model_requires_guest(self, filter_rights_mock):
         self.client.force_login(self.user3)
 
-        with self.assertNumQueries(4):
+        with self.assertNumQueries(2):
             response = self.client.get(reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}))
             self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
 
-    def test_retrieve_model_public(self):
-        self.assertFalse(self.model1.memberships.filter(user=self.user3).exists())
-        self.model1.public = True
-        self.model1.save()
-        self.client.force_login(self.user3)
-
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}))
-            self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        self.assertDictEqual(response.json(), {
-            "id": str(self.model1.id),
-            "created": self.model1.created.isoformat().replace("+00:00", "Z"),
-            "updated": self.model1.updated.isoformat().replace("+00:00", "Z"),
-            "name": "First Model",
-            "description": "first",
-            "rights": ["read"],
-            "archived": False,
-        })
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(self.user3, Model, Role.Guest.value))
 
     def test_retrieve_model(self):
-        self.assertFalse(self.model2.public)
-        self.assertTrue(self.model2.memberships.filter(user=self.user3).exists())
         self.client.force_login(self.user3)
 
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:model-retrieve", kwargs={"pk": str(self.model2.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
 
@@ -317,7 +304,7 @@ class TestModelAPI(FixtureAPITestCase):
             "updated": self.model2.updated.isoformat().replace("+00:00", "Z"),
             "name": "Second Model",
             "description": "",
-            "rights": ["read"],
+            "rights": ["read", "write", "admin"],
             "archived": False,
         })
 
@@ -336,24 +323,29 @@ class TestModelAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_update_model_requires_contrib(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_update_model_requires_contributor(self, has_access_mock):
         self.assertFalse(self.model1.public)
         self.model1.memberships.create(user=self.user3, level=Role.Guest.value)
         self.client.force_login(self.user3)
-        with self.assertNumQueries(5):
+
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                 {"name": "new name", "description": "test"},
                 format="json",
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.model1.refresh_from_db()
         self.assertEqual((self.model1.name, self.model1.description), ("First Model", "first"))
         self.assertDictEqual(response.json(), {"detail": "You do not have a contributor access to this model."})
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user3, self.model1, Role.Contributor.value, skip_public=False))
 
     def test_update_model_unique_name(self):
         self.client.force_login(self.user1)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.put(
                 reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                 {"name": self.model2.name, "description": "", "public": True},
@@ -362,21 +354,22 @@ class TestModelAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"name": ["A model with this name already exists"]})
 
-    def test_update_model_archived_requires_archivable(self):
-        self.assertFalse(self.model1.is_archivable(self.user2))
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Contributor.value)
+    def test_update_model_archived_requires_archivable(self, get_max_level_mock):
         self.client.force_login(self.user2)
+        self.assertFalse(self.model1.is_archivable(self.user2))
 
         cases = [
             (timezone.now(), False),
             (None, True),
         ]
-
         for current_value, new_value in cases:
             with self.subTest(current_value=current_value, new_value=new_value):
+                get_max_level_mock.reset_mock()
                 self.model1.archived = current_value
                 self.model1.save()
 
-                with self.assertNumQueries(8):
+                with self.assertNumQueries(4):
                     response = self.client.put(
                         reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                         {"name": "new name", "description": "test", "archived": new_value},
@@ -385,6 +378,8 @@ class TestModelAPI(FixtureAPITestCase):
                     self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
                 self.assertDictEqual(response.json(), {"archived": ["You are not allowed to archive or unarchive this model."]})
+                self.assertEqual(get_max_level_mock.call_count, 1)
+                self.assertEqual(get_max_level_mock.call_args, call(self.user2, self.model1))
 
     def test_update_model_archived(self):
         self.assertTrue(self.model1.is_archivable(self.user1))
@@ -400,7 +395,7 @@ class TestModelAPI(FixtureAPITestCase):
                 self.model1.archived = current_value
                 self.model1.save()
 
-                with self.assertNumQueries(10):
+                with self.assertNumQueries(5):
                     response = self.client.put(
                         reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                         {"name": "new name", "description": "test", "archived": new_value},
@@ -425,7 +420,7 @@ class TestModelAPI(FixtureAPITestCase):
     def test_update_model(self):
         self.assertFalse(self.model1.public)
         self.client.force_login(self.user1)
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(5):
             response = self.client.put(
                 reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                 {"name": "new name", "description": "", "public": True},
@@ -465,24 +460,29 @@ class TestModelAPI(FixtureAPITestCase):
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
 
-    def test_partial_update_model_requires_contrib(self):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_partial_update_model_requires_contributor(self, has_access_mock):
         self.assertFalse(self.model1.public)
-        self.model1.memberships.create(user=self.user3, level=Role.Guest.value)
         self.client.force_login(self.user3)
-        with self.assertNumQueries(5):
+
+        with self.assertNumQueries(3):
             response = self.client.patch(
                 reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                 {"description": "test"},
                 format="json",
             )
             self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.model1.refresh_from_db()
         self.assertEqual((self.model1.name, self.model1.description), ("First Model", "first"))
         self.assertDictEqual(response.json(), {"detail": "You do not have a contributor access to this model."})
 
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user3, self.model1, Role.Contributor.value, skip_public=False))
+
     def test_partial_update_model(self):
         self.client.force_login(self.user1)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.patch(
                 reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                 {"description": "new desc", "public": True},
@@ -506,7 +506,8 @@ class TestModelAPI(FixtureAPITestCase):
             }
         )
 
-    def test_partial_update_model_archived_requires_archivable(self):
+    @patch("arkindex.users.utils.get_max_level", return_value=Role.Contributor.value)
+    def test_partial_update_model_archived_requires_archivable(self, get_max_level_mock):
         self.assertFalse(self.model1.is_archivable(self.user2))
         self.client.force_login(self.user2)
 
@@ -514,13 +515,13 @@ class TestModelAPI(FixtureAPITestCase):
             (timezone.now(), False),
             (None, True),
         ]
-
         for current_value, new_value in cases:
             with self.subTest(current_value=current_value, new_value=new_value):
+                get_max_level_mock.reset_mock()
                 self.model1.archived = current_value
                 self.model1.save()
 
-                with self.assertNumQueries(7):
+                with self.assertNumQueries(3):
                     response = self.client.patch(
                         reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                         {"archived": new_value},
@@ -529,6 +530,8 @@ class TestModelAPI(FixtureAPITestCase):
                     self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
                 self.assertDictEqual(response.json(), {"archived": ["You are not allowed to archive or unarchive this model."]})
+                self.assertEqual(get_max_level_mock.call_count, 1)
+                self.assertEqual(get_max_level_mock.call_args, call(self.user2, self.model1))
 
     def test_partial_update_model_archived(self):
         self.assertTrue(self.model1.is_archivable(self.user1))
@@ -544,7 +547,7 @@ class TestModelAPI(FixtureAPITestCase):
                 self.model1.archived = current_value
                 self.model1.save()
 
-                with self.assertNumQueries(9):
+                with self.assertNumQueries(4):
                     response = self.client.patch(
                         reverse("api:model-retrieve", kwargs={"pk": str(self.model1.id)}),
                         {"archived": new_value},
@@ -569,59 +572,77 @@ class TestModelAPI(FixtureAPITestCase):
     def test_list_model_versions_requires_logged_in(self):
         """To list a model's versions, you need to be logged in.
         """
-        response = self.client.get(reverse("api:model-versions", kwargs={"pk": str(self.model1.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        with self.assertNumQueries(0):
+            response = self.client.get(reverse("api:model-versions", kwargs={"pk": str(self.model1.id)}))
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
-    def test_list_model_versions_requires_guest_access(self):
-        """To view a model's version, you need guest access on the model
-        """
+    @patch("arkindex.training.api.get_max_level", return_value=None)
+    def test_list_model_versions_requires_guest_access(self, get_max_level_mock):
         self.client.force_login(self.user1)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:model-versions", kwargs={"pk": str(self.model2.id)}))
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "You need a Guest access to list versions of a model."})
 
-    def test_list_model_versions_low_access(self):
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Guest.value)
+    def test_list_model_versions_low_access(self, get_max_level_mock):
         """With only guest access rights on a model, you only see the available versions with a set tag
         """
         self.client.force_login(self.user3)
-        with self.assertNumQueries(8):
+
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:model-versions", kwargs={"pk": str(self.model2.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertListEqual(
             [version["id"] for version in response.json()["results"]],
             [str(self.model_version3.id)],
         )
 
-    def test_list_model_versions_full_access(self):
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user3, self.model2))
+
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Contributor.value)
+    def test_list_model_versions_full_access(self, get_max_level_mock):
         """With contributor access rights, you see all versions.
         """
         self.client.force_login(self.user2)
-        with self.assertNumQueries(8):
+
+        with self.assertNumQueries(5):
             response = self.client.get(reverse("api:model-versions", kwargs={"pk": str(self.model2.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertListEqual(
             [version["id"] for version in response.json()["results"]],
             [str(self.model_version4.id), str(self.model_version3.id)],
         )
 
-    def test_destroy_model_versions_requires_admin(self):
-        """To destroy a model version, you need admin rights on the model.
-        """
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user2, self.model2))
+
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Contributor.value)
+    def test_destroy_model_versions_requires_admin(self, get_max_level_mock):
         self.client.force_login(self.user2)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version1.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You need an Admin access to the model to destroy this version."})
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user2, self.model1))
+
     def test_destroy_model_versions_archived(self):
         self.model1.archived = timezone.now()
         self.model1.save()
         self.client.force_login(self.user1)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.delete(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version1.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -632,20 +653,27 @@ class TestModelAPI(FixtureAPITestCase):
         This also deletes every worker run that used this model version
         """
         self.client.force_login(self.user1)
-        with self.assertNumQueries(12):
+        with self.assertNumQueries(9):
             response = self.client.delete(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version1.id)}))
             self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
 
-    def test_retrieve_model_versions_require_guest(self):
-        """To retrieve a model version with a set tag and state==Available, you need guest rights on the model.
+    @patch("arkindex.training.api.get_max_level", return_value=None)
+    def test_retrieve_model_versions_require_guest(self, get_max_level_mock):
+        """
+        To retrieve a model version with a set tag and state==Available, you need guest rights on the model.
         """
         self.client.force_login(self.user1)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version3.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You need a Guest access to the model to retrieve this version."})
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user1, self.model2))
 
-    def test_retrieve_model_versions_tag_available(self):
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Guest.value)
+    def test_retrieve_model_versions_tag_available(self, get_max_level_mock):
         """
         Retrieve a model version with a set tag and state==Available with guest rights on the model.
         No s3 URL is set with a guest access.
@@ -653,9 +681,11 @@ class TestModelAPI(FixtureAPITestCase):
         self.assertIsNotNone(self.model_version3.tag)
         self.assertEqual(self.model_version3.state, ModelVersionState.Available)
         self.client.force_login(self.user3)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version3.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "id": str(self.model_version3.id),
             "model_id": str(self.model2.id),
@@ -670,38 +700,55 @@ class TestModelAPI(FixtureAPITestCase):
             "s3_url": None,
             "s3_put_url": None,
         })
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user3, self.model2))
 
-    def test_retrieve_model_versions_no_tag_requires_contributor(self):
-        """To retrieve a model version with no set tag or state!=Available, you need contributor rights on the model.
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Guest.value)
+    def test_retrieve_model_versions_no_tag_requires_contributor(self, get_max_level_mock):
+        """
+        To retrieve a model version with no set tag or state!=Available, you need contributor rights on the model.
         """
         self.assertEqual(self.model_version4.tag, None)
         self.client.force_login(self.user3)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version4.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You need a Contributor access to the model to retrieve this version."})
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user3, self.model2))
 
-    def test_retrieve_model_versions_no_validated_requires_contributor(self):
-        """To retrieve a model version with no set tag or state!=Available, you need contributor rights on the model.
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Guest.value)
+    def test_retrieve_model_versions_no_validated_requires_contributor(self, get_max_level_mock):
+        """
+        To retrieve a model version with no set tag or state!=Available, you need contributor rights on the model.
         """
         self.model_version4.tag = "A tag"
         self.model_version4.state = ModelVersionState.Error
         self.model_version4.save()
         self.client.force_login(self.user3)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version4.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You need a Contributor access to the model to retrieve this version."})
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user3, self.model2))
 
-    @patch("arkindex.project.aws.s3.meta.client.generate_presigned_url")
-    def test_retrieve_model_versions(self, s3_presigned_url):
-        """Retrieve a model version with no set tag or state!=Available with contributor rights on the model.
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Contributor.value)
+    @patch("arkindex.project.aws.s3.meta.client.generate_presigned_url", return_value="http://s3/get_url")
+    def test_retrieve_model_versions(self, s3_presigned_url, get_max_level_mock):
+        """
+        Retrieve a model version with no set tag or state!=Available with contributor rights on the model.
         """
-        s3_presigned_url.return_value = "http://s3/get_url"
         self.client.force_login(self.user2)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version4.id)}))
             self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         self.assertDictEqual(response.json(), {
             "id": str(self.model_version4.id),
             "model_id": str(self.model2.id),
@@ -717,24 +764,28 @@ class TestModelAPI(FixtureAPITestCase):
             "s3_put_url": None,
         })
 
-    @patch("arkindex.project.aws.s3.Object")
-    @patch("arkindex.project.aws.S3FileMixin.exists")
-    def test_partial_update_model_version_requires_contributor(self, exists, s3_object):
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user2, self.model2))
+
+    @patch("arkindex.training.api.get_max_level", return_value=Role.Guest.value)
+    def test_partial_update_model_version_requires_contributor(self, get_max_level_mock):
         """
         Can't partial update a model version as guest
         """
-        s3_object().content_length = self.model_version3.size
-        s3_object().e_tag = self.model_version3.archive_hash
-        exists.return_value = True
         self.client.force_login(self.user3)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.patch(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version3.id)}), {"state": "available"})
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(
             response.json(),
             {"detail": "You need a Contributor access to the model to update this version."}
         )
 
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user3, self.model2))
+
     @patch("arkindex.project.aws.s3.Object")
     @patch("arkindex.project.aws.S3FileMixin.exists")
     def test_partial_update_model_version_archived(self, exists, s3_object):
@@ -745,7 +796,7 @@ class TestModelAPI(FixtureAPITestCase):
         self.model1.save()
         self.client.force_login(self.user1)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.patch(reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version1.id)}), {"state": "available"})
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -775,7 +826,7 @@ class TestModelAPI(FixtureAPITestCase):
             "archive_hash": "n" * 32,
         }
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version1.id)}),
                 request,
@@ -824,7 +875,7 @@ class TestModelAPI(FixtureAPITestCase):
             "archive_hash": "n" * 32,
         }
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(6):
             response = self.client.patch(
                 reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version3.id)}),
                 request,
@@ -860,7 +911,7 @@ class TestModelAPI(FixtureAPITestCase):
         self.model_version3.parent = self.model_version2
         self.model_version3.save()
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.patch(
                 reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version3.id)}),
                 {"parent": None},
@@ -886,17 +937,14 @@ class TestModelAPI(FixtureAPITestCase):
         self.model_version3.refresh_from_db()
         self.assertIsNone(self.model_version3.parent_id)
 
-    @patch("arkindex.project.aws.s3.Object")
-    @patch("arkindex.project.aws.S3FileMixin.exists")
-    def test_update_model_version_requires_contributor(self, exists, s3_object):
+    @patch("arkindex.training.api.get_max_level", return_value=None)
+    def test_update_model_version_requires_contributor(self, get_max_level_mock):
         """
         Can't update a model version with guest access rights to the model
         """
-        s3_object().content_length = self.model_version3.size
-        s3_object().e_tag = self.model_version3.archive_hash
-        exists.return_value = True
         self.client.force_login(self.user3)
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version3.id)}),
                 {
@@ -905,12 +953,15 @@ class TestModelAPI(FixtureAPITestCase):
                     "size": 8,
                     "state": ModelVersionState.Available.value,
                     "description": "other description",
-                    "configuration": {"hi": "Who am I ?"},
+                    "configuration": {"hi": "Who am I?"},
                 },
                 format="json",
             )
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You need a Contributor access to the model to update this version."})
+        self.assertEqual(get_max_level_mock.call_count, 1)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user3, self.model2))
 
     @patch("arkindex.project.aws.s3.Object")
     @patch("arkindex.project.aws.S3FileMixin.exists")
@@ -922,7 +973,7 @@ class TestModelAPI(FixtureAPITestCase):
         self.model1.save()
         self.client.force_login(self.user1)
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.put(
                 reverse("api:model-version-retrieve", kwargs={"pk": str(self.model_version1.id)}),
                 {
@@ -939,21 +990,23 @@ class TestModelAPI(FixtureAPITestCase):
 
         self.assertDictEqual(response.json(), {"model": ["This model is archived."]})
 
-    @patch("arkindex.project.aws.S3FileMixin.exists")
-    def test_validate_model_version_requires_contributor(self, exists):
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_validate_model_version_requires_contributor(self, has_access_mock):
         self.client.force_login(self.user3)
-        exists.return_value = False
-        with self.assertNumQueries(6):
+
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:model-version-validate", kwargs={"pk": str(self.model_version2.id)}),
                 {"archive_hash": "x" * 32, "hash": "y" * 32, "size": 32},
                 format="json"
             )
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {
             "detail": "You need a Contributor access to the model to validate this version."
         })
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user3, self.model1, Role.Contributor.value, skip_public=False))
 
     @patch("arkindex.project.aws.S3FileMixin.exists")
     def test_validate_model_version_archived(self, exists):
@@ -962,7 +1015,7 @@ class TestModelAPI(FixtureAPITestCase):
         self.client.force_login(self.user1)
         exists.return_value = False
 
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:model-version-validate", kwargs={"pk": str(self.model_version1.id)}),
                 {"archive_hash": "x" * 32, "hash": "y" * 32, "size": 32},
@@ -976,7 +1029,7 @@ class TestModelAPI(FixtureAPITestCase):
     def test_validate_model_version_required_fields(self, exists):
         self.client.force_login(self.user1)
         exists.return_value = False
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:model-version-validate", kwargs={"pk": str(self.model_version1.id)}),
                 {},
@@ -996,7 +1049,7 @@ class TestModelAPI(FixtureAPITestCase):
         """
         self.client.force_login(self.user1)
         exists.return_value = False
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:model-version-validate", kwargs={"pk": str(self.model_version1.id)}),
                 {"archive_hash": "d" * 32, "hash": "e" * 32, "size": 32},
@@ -1016,7 +1069,7 @@ class TestModelAPI(FixtureAPITestCase):
         """
         self.client.force_login(self.user1)
         s3_url_mock.return_value = "http://s3/archive_url"
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:model-version-validate", kwargs={"pk": str(self.model_version1.id)}),
                 {"archive_hash": "d" * 32, "hash": str(self.model_version5.hash), "size": 32},
@@ -1050,7 +1103,7 @@ class TestModelAPI(FixtureAPITestCase):
         exists.return_value = True
         s3_object().content_length = 31
         self.client.force_login(self.user1)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:model-version-validate", kwargs={"pk": str(self.model_version1.id)}),
                 {"archive_hash": "d" * 32, "hash": "e" * 32, "size": 32},
@@ -1072,7 +1125,7 @@ class TestModelAPI(FixtureAPITestCase):
         s3_object().content_length = 32
         s3_object().e_tag = "a" * 32
         self.client.force_login(self.user1)
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:model-version-validate", kwargs={"pk": str(self.model_version1.id)}),
                 {"archive_hash": "d" * 32, "hash": "e" * 32, "size": 32},
@@ -1151,23 +1204,28 @@ class TestModelAPI(FixtureAPITestCase):
         request = {
             "name": self.model1.name,
         }
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(3):
             response = self.client.post(reverse("api:models"), request)
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
         self.assertDictEqual(response.json(), {"id": str(self.model1.id), "name": "A model with this name already exists"})
 
-    def test_create_model_name_taken_no_access(self):
-        """Raises a 403 with no additional information when creating a model with a name that is already used for another model
+    @patch("arkindex.project.mixins.has_access", return_value=False)
+    def test_create_model_name_taken_no_access(self, has_access_mock):
+        """
+        Raises a 403 with no additional information when creating a model with a name that is already used for another model
         but without access rights on this model
         """
         self.client.force_login(self.user3)
-        request = {
-            "name": self.model1.name,
-        }
-        with self.assertNumQueries(6):
-            response = self.client.post(reverse("api:models"), request)
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+        with self.assertNumQueries(3):
+            response = self.client.post(reverse("api:models"), {
+                "name": self.model1.name,
+            })
+            self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
         self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
+        self.assertEqual(has_access_mock.call_count, 1)
+        self.assertEqual(has_access_mock.call_args, call(self.user3, self.model1, Role.Guest.value, skip_public=False))
 
     def test_list_models_requires_verified(self):
         user = User.objects.create(display_name="Not Verified", verified_email=False)
@@ -1181,38 +1239,13 @@ class TestModelAPI(FixtureAPITestCase):
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
         self.assertDictEqual(response.json(), {"detail": "Authentication credentials were not provided."})
 
-    def test_list_models_low_access(self):
-        """User 3 has only access to Model2 with a guest access.
-        He only sees model_version3 of Model2 since it's the only one that
-        has a set tag and is in Available state.
-        """
-        self.client.force_login(self.user3)
-        with self.assertNumQueries(7):
-            response = self.client.get(reverse("api:models"))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        models = response.json()["results"]
-
-        self.assertListEqual(models, [
-            {
-                "id": str(self.model2.id),
-                "created": self.model2.created.isoformat().replace("+00:00", "Z"),
-                "updated": self.model2.updated.isoformat().replace("+00:00", "Z"),
-                "name": "Second Model",
-                "description": "",
-                "rights": ["read"],
-                "archived": False,
-            }
-        ])
-
-    def test_list_models_contrib_access(self):
-        """User 2 has contributor access to Model1 and Model2.
-        He has contributor access on both that's why he sees all related versions.
-        Models list is ordered by name, first Model1 (named 'First Model') then Model2 (named 'Second Model').
-        """
+    def test_list(self):
         self.client.force_login(self.user2)
-        with self.assertNumQueries(8):
+
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:models"))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
+            self.assertEqual(response.status_code, status.HTTP_200_OK)
+
         models = response.json()["results"]
 
         self.assertListEqual(models, [
@@ -1222,7 +1255,7 @@ class TestModelAPI(FixtureAPITestCase):
                 "updated": self.model1.updated.isoformat().replace("+00:00", "Z"),
                 "name": "First Model",
                 "description": "first",
-                "rights": ["read", "write"],
+                "rights": ["read", "write", "admin"],
                 "archived": False,
             },
             {
@@ -1233,14 +1266,23 @@ class TestModelAPI(FixtureAPITestCase):
                 "description": "",
                 "rights": ["read", "write", "admin"],
                 "archived": False,
-            }
+            },
+            {
+                "id": str(self.model3.id),
+                "created": self.model3.created.isoformat().replace("+00:00", "Z"),
+                "updated": self.model3.updated.isoformat().replace("+00:00", "Z"),
+                "name": "Third Model",
+                "description": "",
+                "rights": ["read", "write", "admin"],
+                "archived": False,
+            },
         ])
 
     def test_list_models_filter_name(self):
         """User 2 has access to both Models, use search parameter name=Second returns only Model2
         """
         self.client.force_login(self.user2)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:models"), {"name": "second"})
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         models = response.json()["results"]
@@ -1259,7 +1301,7 @@ class TestModelAPI(FixtureAPITestCase):
 
     def test_list_models_filter_compatible_worker(self):
         self.client.force_login(self.user2)
-        with self.assertNumQueries(7):
+        with self.assertNumQueries(4):
             response = self.client.get(reverse("api:models"), {"compatible_worker": str(self.worker.id)})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         models = response.json()["results"]
@@ -1289,7 +1331,7 @@ class TestModelAPI(FixtureAPITestCase):
                     "updated": self.model1.updated.isoformat().replace("+00:00", "Z"),
                     "name": "First Model",
                     "description": "first",
-                    "rights": ["read", "write"],
+                    "rights": ["read", "write", "admin"],
                     "archived": True,
                 }
             ]),
@@ -1302,19 +1344,28 @@ class TestModelAPI(FixtureAPITestCase):
                     "description": "",
                     "rights": ["read", "write", "admin"],
                     "archived": False,
-                }
+                },
+                {
+                    "id": str(self.model3.id),
+                    "created": self.model3.created.isoformat().replace("+00:00", "Z"),
+                    "updated": self.model3.updated.isoformat().replace("+00:00", "Z"),
+                    "name": "Third Model",
+                    "description": "",
+                    "rights": ["read", "write", "admin"],
+                    "archived": False,
+                },
             ]),
         ]
 
         for archived, expected_results in cases:
-            with self.subTest(archived=archived), self.assertNumQueries(7):
+            with self.subTest(archived=archived), self.assertNumQueries(4):
                 response = self.client.get(reverse("api:models"), {"archived": archived})
                 self.assertEqual(response.status_code, status.HTTP_200_OK)
                 self.assertEqual(response.json()["results"], expected_results)
 
     def test_list_models_filter_compatible_worker_doesnt_exist(self):
         self.client.force_login(self.user2)
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:models"), {"compatible_worker": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         models = response.json()["results"]
@@ -1324,7 +1375,7 @@ class TestModelAPI(FixtureAPITestCase):
     def test_list_models_filter_compatible_worker_no_models(self):
         self.client.force_login(self.user2)
         dla_worker = Worker.objects.get(slug="dla")
-        with self.assertNumQueries(5):
+        with self.assertNumQueries(3):
             response = self.client.get(reverse("api:models"), {"compatible_worker": str(dla_worker.id)})
             self.assertEqual(response.status_code, status.HTTP_200_OK)
         models = response.json()["results"]
diff --git a/arkindex/training/tests/test_model_compatible_worker.py b/arkindex/training/tests/test_model_compatible_worker.py
index f163dc0e0c..20a19ff5b7 100644
--- a/arkindex/training/tests/test_model_compatible_worker.py
+++ b/arkindex/training/tests/test_model_compatible_worker.py
@@ -1,9 +1,10 @@
 from datetime import datetime, timezone
+from unittest.mock import call, patch
 
 from django.urls import reverse
 from rest_framework import status
 
-from arkindex.process.models import Worker
+from arkindex.process.models import Repository, Worker
 from arkindex.project.tests import FixtureAPITestCase
 from arkindex.training.models import Model
 from arkindex.users.models import Role
@@ -63,7 +64,7 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
     def test_create_model_not_found(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.worker1.id),
@@ -76,11 +77,18 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
             "model": [f'Invalid pk "{self.worker1.id}" - object does not exist.'],
         })
 
-    def test_create_requires_model_contributor(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_create_requires_model_contributor(self, filter_rights_mock):
         self.model1.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        filter_rights_mock.side_effect = [
+            Model.objects.none(),
+            Worker.objects.all(),
+            Repository.objects.all(),
+        ]
+
+        with self.assertNumQueries(3):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -93,27 +101,16 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
             "model": [f'Invalid pk "{self.model1.id}" - object does not exist.'],
         })
 
-    def test_create_ignores_model_public(self):
-        self.client.force_login(self.user)
-
-        with self.assertNumQueries(8):
-            response = self.client.post(
-                reverse("api:model-compatible-worker-manage", kwargs={
-                    # User has no access to model2 and model2 is public
-                    "model": str(self.model2.id),
-                    "worker": str(self.worker2.id),
-                })
-            )
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
-        self.assertEqual(response.json(), {
-            "model": [f'Invalid pk "{self.model2.id}" - object does not exist.'],
-        })
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Model, Role.Contributor.value),
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
 
     def test_create_worker_not_found(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -126,11 +123,18 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
             "worker": [f'Invalid pk "{self.model1.id}" - object does not exist.'],
         })
 
-    def test_create_requires_worker_contributor(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_create_requires_worker_contributor(self, filter_rights_mock):
         self.worker2.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        filter_rights_mock.side_effect = [
+            Model.objects.all(),
+            Worker.objects.none(),
+            Repository.objects.none(),
+        ]
+
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -143,10 +147,16 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
             "worker": [f'Invalid pk "{self.worker2.id}" - object does not exist.'],
         })
 
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Model, Role.Contributor.value),
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
+
     def test_create_nothing_found(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.worker2.id),
@@ -164,12 +174,10 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
         self.client.force_login(self.user)
 
         # 2 queries to retrieve the user and session
-        # 4 queries for the various content types for memberships,
-        # which are normally cached between requests but reset between each unit test
         # 2 queries to retrieve the model and worker
         # 1 query to check that the relationship does not already exist
         # 1 query to insert
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -199,7 +207,7 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
         self.worker1.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -255,7 +263,7 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
         self.model1.compatible_workers.add(self.worker2)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(5):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -283,7 +291,7 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
         self.model1.save()
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -341,7 +349,7 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
     def test_destroy_model_not_found(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.worker2.id),
@@ -354,11 +362,18 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
             "model": [f'Invalid pk "{self.worker2.id}" - object does not exist.'],
         })
 
-    def test_destroy_requires_model_contributor(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_destroy_requires_model_contributor(self, filter_rights_mock):
         self.model1.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        filter_rights_mock.side_effect = [
+            Model.objects.none(),
+            Worker.objects.all(),
+            Repository.objects.all(),
+        ]
+
+        with self.assertNumQueries(3):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -371,27 +386,16 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
             "model": [f'Invalid pk "{self.model1.id}" - object does not exist.'],
         })
 
-    def test_destroy_ignores_model_public(self):
-        self.client.force_login(self.user)
-
-        with self.assertNumQueries(8):
-            response = self.client.delete(
-                reverse("api:model-compatible-worker-manage", kwargs={
-                    # User has no access to model2 and model2 is public
-                    "model": str(self.model2.id),
-                    "worker": str(self.worker2.id),
-                })
-            )
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
-        self.assertEqual(response.json(), {
-            "model": [f'Invalid pk "{self.model2.id}" - object does not exist.'],
-        })
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Model, Role.Contributor.value),
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
 
     def test_destroy_worker_not_found(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -404,11 +408,18 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
             "worker": [f'Invalid pk "{self.model1.id}" - object does not exist.'],
         })
 
-    def test_destroy_requires_worker_contributor(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights")
+    def test_destroy_requires_worker_contributor(self, filter_rights_mock):
         self.worker2.memberships.filter(user=self.user).update(level=Role.Guest.value)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(7):
+        filter_rights_mock.side_effect = [
+            Model.objects.all(),
+            Worker.objects.none(),
+            Repository.objects.none(),
+        ]
+
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -421,10 +432,16 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
             "worker": [f'Invalid pk "{self.worker2.id}" - object does not exist.'],
         })
 
+        self.assertListEqual(filter_rights_mock.call_args_list, [
+            call(self.user, Model, Role.Contributor.value),
+            call(self.user, Worker, Role.Contributor.value),
+            call(self.user, Repository, Role.Contributor.value),
+        ])
+
     def test_destroy_nothing_found(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.worker2.id),
@@ -444,12 +461,10 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
         self.model1.compatible_workers.set([self.worker1, self.worker2])
 
         # 2 queries to retrieve the user and session
-        # 4 queries for the various content types for memberships,
-        # which are normally cached between requests but reset between each unit test
         # 2 queries to retrieve the model and worker
         # 1 query to retrieve the existing relationship
         # 1 query to delete
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -476,7 +491,7 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
         self.model1.compatible_workers.add(self.worker2)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(10):
+        with self.assertNumQueries(6):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -524,7 +539,7 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
     def test_destroy_does_not_exist(self):
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(9):
+        with self.assertNumQueries(5):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
@@ -553,7 +568,7 @@ class TestModelCompatibleWorkerManage(FixtureAPITestCase):
         self.model1.compatible_workers.add(self.worker2)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(8):
+        with self.assertNumQueries(4):
             response = self.client.delete(
                 reverse("api:model-compatible-worker-manage", kwargs={
                     "model": str(self.model1.id),
diff --git a/arkindex/users/admin.py b/arkindex/users/admin.py
index 3d9d1a9129..a50a388cf8 100644
--- a/arkindex/users/admin.py
+++ b/arkindex/users/admin.py
@@ -3,11 +3,10 @@ from django.contrib import admin
 from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
 from django.contrib.auth.forms import ReadOnlyPasswordHashField
 from django.contrib.auth.models import Group as BaseGroup
-from django.contrib.contenttypes.admin import GenericTabularInline
 from django.db.models.functions import Collate
 from enumfields.admin import EnumFieldListFilter
 
-from arkindex.users.models import Group, Right, User, UserScope
+from arkindex.users.models import User, UserScope
 
 
 class UserCreationForm(forms.ModelForm):
@@ -95,36 +94,7 @@ class UserScopeAdmin(admin.ModelAdmin):
     list_filter = [("scope", EnumFieldListFilter), ]
 
 
-class GroupMembershipInline(GenericTabularInline):
-    ct_fk_field = "content_id"
-    model = Right
-    fields = ("group", "level")
-    extra = 1
-
-    def get_queryset(self, request):
-        qs = super().get_queryset(request)
-        return qs.filter(user__isnull=True)
-
-
-class UserMembershipInline(GenericTabularInline):
-    ct_fk_field = "content_id"
-    model = Right
-    fields = ("user", "level")
-    extra = 1
-
-    def get_queryset(self, request):
-        qs = super().get_queryset(request)
-        return qs.filter(group__isnull=True)
-
-
-class GroupAdmin(admin.ModelAdmin):
-    list_display = ("id", "name", "public")
-    inlines = (UserMembershipInline, )
-
-
 admin.site.register(User, UserAdmin)
-# Register the custom GroupAdmin
-admin.site.register(Group, GroupAdmin)
-# and hide base GroupAdmin form contrib.auth
-admin.site.unregister(BaseGroup)
 admin.site.register(UserScope, UserScopeAdmin)
+# Remove the admin page for the default Django groups
+admin.site.unregister(BaseGroup)
diff --git a/arkindex/users/allow_all.py b/arkindex/users/allow_all.py
new file mode 100644
index 0000000000..2704635a32
--- /dev/null
+++ b/arkindex/users/allow_all.py
@@ -0,0 +1,28 @@
+from typing import Optional
+
+from django.db.models import IntegerField, Value
+
+from arkindex.users.models import Role, User
+
+
+def has_access(user: User, instance, level: int, skip_public: bool = False) -> bool:
+    """
+    Check if the user has access to a generic instance with a minimum level
+    If skip_public parameter is set to true, exclude rights on public instances
+    """
+    return True
+
+
+def filter_rights(user: User, model, level: int):
+    """
+    Return a generic queryset of objects with access rights for this user.
+    Level filtering parameter should be an integer between 1 and 100.
+    """
+    return model.objects.annotate(max_level=Value(Role.Admin.value, IntegerField()))
+
+
+def get_max_level(user: User, instance) -> Optional[int]:
+    """
+    Returns the maximum access level on a given model instance
+    """
+    return Role.Admin.value
diff --git a/arkindex/users/api.py b/arkindex/users/api.py
index 656111cd18..5c4b9c40df 100644
--- a/arkindex/users/api.py
+++ b/arkindex/users/api.py
@@ -1,15 +1,11 @@
 import logging
 import urllib.parse
-from uuid import UUID
 
 from django.conf import settings
 from django.contrib.auth import login, logout
 from django.contrib.auth.forms import PasswordResetForm
 from django.contrib.auth.tokens import default_token_generator
-from django.contrib.contenttypes.models import ContentType
 from django.core.mail import send_mail
-from django.db.models import Count, Prefetch, Q
-from django.shortcuts import get_object_or_404
 from django.template.loader import render_to_string
 from django.urls import reverse
 from django.utils.http import urlsafe_base64_encode
@@ -19,40 +15,31 @@ from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
 from rest_framework import serializers, status
 from rest_framework.exceptions import AuthenticationFailed, NotFound, PermissionDenied, ValidationError
-from rest_framework.generics import (
-    CreateAPIView,
-    ListAPIView,
-    ListCreateAPIView,
-    RetrieveDestroyAPIView,
-    RetrieveUpdateDestroyAPIView,
-)
-from rest_framework.permissions import SAFE_METHODS
+from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveDestroyAPIView, RetrieveUpdateDestroyAPIView
 from rest_framework.response import Response
 from rest_framework.views import APIView
 from rq.job import JobStatus
 
 from arkindex.documents.models import Corpus
-from arkindex.process.models import Worker
-from arkindex.project.mixins import ACLMixin, WorkerACLMixin
 from arkindex.project.permissions import IsAuthenticatedOrReadOnly, IsVerified
-from arkindex.users.models import Group, Right, Role, Scope, User, UserScope
+from arkindex.users.models import Group, Role, Scope, User, UserScope
 from arkindex.users.serializers import (
     EmailLoginSerializer,
-    GenericMembershipSerializer,
-    GroupSerializer,
     JobSerializer,
-    MemberGroupSerializer,
-    MembershipCreateSerializer,
-    MembershipSerializer,
     NewUserSerializer,
     PasswordResetConfirmSerializer,
     PasswordResetSerializer,
     UserSerializer,
 )
-from arkindex.users.utils import RightContent, get_max_level
 
 logger = logging.getLogger(__name__)
 
+# Process tasks running in RQ are hidden from user jobs
+VISIBLE_QUEUES = [
+    q for q in QUEUES.keys()
+    if q != "tasks"
+]
+
 
 @extend_schema(tags=["users"])
 @extend_schema_view(
@@ -290,23 +277,6 @@ class PasswordResetConfirm(CreateAPIView):
     serializer_class = PasswordResetConfirmSerializer
 
 
-@extend_schema(tags=["users"])
-class GroupsList(ListCreateAPIView):
-    """
-    List existing groups either if they are public or the request user is a member.
-    A POST request allow to create a new group.
-    """
-    serializer_class = GroupSerializer
-    permission_classes = (IsVerified, )
-
-    def get_queryset(self):
-        # List public groups and ones to which user belongs
-        return Group.objects \
-            .annotate(members_count=Count("users")) \
-            .filter(Q(public=True) | Q(users__in=[self.request.user])) \
-            .order_by("name", "id")
-
-
 @extend_schema(tags=["jobs"])
 class JobList(ListAPIView):
     """
@@ -340,7 +310,7 @@ class JobRetrieve(RetrieveDestroyAPIView):
     serializer_class = JobSerializer
 
     def get_object(self):
-        for queue_name in QUEUES.keys():
+        for queue_name in VISIBLE_QUEUES:
             job = get_queue(queue_name).fetch_job(str(self.kwargs["pk"]))
             if not job:
                 continue
@@ -353,287 +323,3 @@ class JobRetrieve(RetrieveDestroyAPIView):
         if instance.get_status(refresh=False) == JobStatus.STARTED:
             raise ValidationError(["Cannot delete a running job."])
         instance.delete()
-
-
-@extend_schema(tags=["users"])
-class GroupsCreate(CreateAPIView):
-    """
-    Create a new group. The request user will be added as a member of this group.
-    """
-    serializer_class = GroupSerializer
-    permission_classes = (IsVerified, )
-    # For OpenAPI type discovery
-    queryset = Group.objects.none()
-
-
-@extend_schema(tags=["users"])
-@extend_schema_view(
-    patch=extend_schema(
-        description="Partially update details of a group. "
-                    "Requires to have `admin` privileges on this group."
-    ),
-    put=extend_schema(
-        description="Update details of a group. "
-                    "Requires to have `admin` privileges on this group."
-    ),
-    delete=extend_schema(
-        description="Delete a group. "
-                    "Requires to have `admin` privileges on this group."
-    )
-)
-class GroupDetails(RetrieveUpdateDestroyAPIView):
-    """
-    Retrieve details about a specific group
-    """
-    serializer_class = GroupSerializer
-    permission_classes = (IsVerified, )
-    # For OpenAPI type discovery
-    queryset = Group.objects.none()
-
-    def get_membership(self, group):
-        try:
-            return group.memberships.get(user_id=self.request.user.id)
-        except Right.DoesNotExist:
-            raise PermissionDenied(detail="Only members of a group can retrieve their details")
-
-    def get_object(self):
-        if not hasattr(self, "_group"):
-            self._group = super().get_object()
-
-            if self.request.user.is_admin:
-                self._group.level = Role.Admin.value
-            else:
-                # Retrieve the membership to know the privilege level for this user
-                if not hasattr(self, "_member"):
-                    self._member = self.get_membership(self._group)
-                # Add request member level to the group
-                self._group.level = self._member.level
-
-        return self._group
-
-    def check_object_permissions(self, request, obj):
-        if request.user.is_admin:
-            return
-        if not hasattr(self, "_member"):
-            self._member = self.get_membership(obj)
-        # Check the user has the right to delete or update a group
-        super().check_object_permissions(request, obj)
-        if request.method not in SAFE_METHODS and self._member.level < Role.Admin.value:
-            raise PermissionDenied(detail="Only members with an admin privilege may update or delete this group.")
-
-    def get_queryset(self):
-        # Superusers have access to all groups
-        if self.request.user.is_authenticated and self.request.user.is_admin:
-            return Group.objects.all() \
-                .annotate(members_count=Count("memberships"))
-        # Filter groups that are public or for which the user is a member
-        return Group.objects \
-            .annotate(members_count=Count("memberships")) \
-            .filter(
-                Q(public=True)
-                | Q(memberships__user=self.request.user)
-            )
-
-
-@extend_schema(tags=["users"])
-class UserMemberships(ListAPIView):
-    """
-    List groups to which the request user belongs with its privileges on each group
-    """
-    serializer_class = MemberGroupSerializer
-    permission_classes = (IsVerified, )
-
-    def get_queryset(self):
-
-        def _build_fake_right(group):
-            # Serialize a fake memberships for admin user with no needs for ID and level
-            admin_membership = Right(
-                content_object=group,
-                user=self.request.user,
-                level=None,
-            )
-            admin_membership.id = None
-            admin_membership.members_count = group.members_count
-            return admin_membership
-
-        if self.request.user.is_authenticated and self.request.user.is_admin:
-            # Allow super admins to access all groups as if they had an admin access level
-            groups = Group.objects.all() \
-                .annotate(members_count=Count("memberships")) \
-                .order_by("name", "id")
-            return [_build_fake_right(group) for group in groups]
-        # Annotate rights with the group member count as Prefetch is not available with the Generic FK
-        return self.request.user.rights \
-            .prefetch_related("content_object") \
-            .filter(content_type=ContentType.objects.get_for_model(Group)) \
-            .annotate(members_count=Count("group_target__memberships")) \
-            .order_by("group_target__name", "group_target__id")
-
-
-@extend_schema(tags=["users"])
-@extend_schema_view(
-    patch=extend_schema(
-        description="Partially update a generic membership."
-    ),
-    put=extend_schema(
-        description="Update a generic membership."
-    ),
-    delete=extend_schema(
-        description="Delete a generic membership."
-    )
-)
-class MembershipDetails(ACLMixin, RetrieveUpdateDestroyAPIView):
-    """
-    Retrieve details of a generic membership
-    """
-    serializer_class = MembershipSerializer
-    permission_classes = (IsVerified, )
-    # Perform access checks with the permissions
-    queryset = Right.objects.select_related("content_type").all()
-
-    def check_object_permissions(self, request, membership):
-        super().check_object_permissions(request, membership)
-
-        # Retrieve access level
-        access_level = None
-        if isinstance(membership.content_object, Worker):
-            # Use WorkerACLMixin method as the worker access level may be inherited from its repository
-            access_level = WorkerACLMixin.get_max_level(self, membership.content_object)
-        else:
-            access_level = get_max_level(self.request.user, membership.content_object)
-
-        # Check the access level of the request user on the content object
-        if not access_level or access_level < Role.Guest.value:
-            raise NotFound
-
-        #  Allow any guest to retrieve information
-        if self.request.method in SAFE_METHODS:
-            return
-
-        # Allow a user to remove its own membership
-        if self.request.method == "DELETE" and membership.user_id == self.request.user.id:
-            return
-
-        # Only allow admins to edit the access level of a membership
-        if access_level < Role.Admin.value:
-            raise PermissionDenied(
-                detail="Only admins of the target membership group can perform this action."
-            )
-
-    def perform_destroy(self, instance):
-        # At least one admin member must exist except for workers which depends on their repositories
-        if (
-            instance.content_type != ContentType.objects.get_for_model(Worker)
-            and instance.level >= Role.Admin.value
-            and instance.content_object.memberships.using("default").filter(level__gte=Role.Admin.value).count() < 2
-        ):
-            raise ValidationError({"detail": ["Removing all memberships with an admin privilege is not possible."]})
-        return super().perform_destroy(instance)
-
-
-MEMBERSHIPS_TYPE_FILTERS = ["user", "group"]
-
-
-@extend_schema(
-    parameters=[
-        OpenApiParameter(
-            content.name,
-            type=OpenApiTypes.UUID,
-            description=f"List user members for a {content}",
-        )
-        for content in RightContent
-    ] + [
-        OpenApiParameter(
-            "type",
-            description="Filter memberships by owner type",
-            enum=MEMBERSHIPS_TYPE_FILTERS,
-        )
-    ],
-    tags=["users"]
-)
-class GenericMembershipsList(WorkerACLMixin, ListAPIView):
-    """
-    List memberships for a specific content with their privileges.
-    Target of the right must be defined as a query parameter.
-    """
-    permission_classes = (IsVerified, )
-    serializer_class = GenericMembershipSerializer
-
-    def get_content_object(self):
-        """
-        Retrieve a generic content based on query parameters.
-        This method does not perform any permission check.
-        """
-        allowed_contents = [c.name for c in RightContent]
-        content_keys = [key for key in self.request.query_params if key in allowed_contents]
-        # Assert exactly one of the content object parameter has been set
-        if len(content_keys) != 1:
-            raise ValidationError({
-                "__all__": [
-                    "Exactly one of those query parameters must be defined: "
-                    f"{', '.join(allowed_contents)}."
-                ]
-            })
-
-        key, = content_keys
-        content_model = RightContent[key].value
-        content_id = self.request.query_params[key]
-        try:
-            content_uuid = UUID(content_id)
-        except (AttributeError, ValueError):
-            raise ValidationError({key: [f"'{content_id}' is not a valid UUID."]})
-
-        # Use default database in case the content object has just been created
-        return get_object_or_404(content_model.objects.using("default"), id=content_uuid)
-
-    def get_queryset(self):
-        content_object = self.get_content_object()
-
-        # Determine the user right on the generic content
-        has_access = None
-        if isinstance(content_object, Worker):
-            # Workers rights may be determined by their repository
-            has_access = self.has_worker_access(content_object, Role.Guest.value)
-        else:
-            access_params = {}
-            if isinstance(content_object, Group):
-                # A user may not be able to list group members if he is not a member
-                access_params["skip_public"] = True
-            has_access = self.has_access(content_object, Role.Guest.value, **access_params)
-
-        # Ensure the user has a guest access to the content
-        if not has_access:
-            raise PermissionDenied(
-                detail="You do not have the required access level to list members for this content."
-            )
-
-        # Avoid a stale read when adding/deletig a member
-        qs = content_object.memberships \
-            .using("default") \
-            .prefetch_related(
-                "user",
-                Prefetch("group", queryset=Group.objects.annotate(members_count=Count("memberships")))
-            ) \
-            .order_by("user__display_name", "group__name", "id")
-
-        # Handle a simple owner type filter
-        type_filter = self.request.query_params.get("type")
-        if type_filter and type_filter in MEMBERSHIPS_TYPE_FILTERS:
-            type_filter = {f"{type_filter}__isnull": False}
-            qs = qs.filter(**type_filter)
-
-        return qs
-
-
-@extend_schema_view(
-    post=extend_schema(
-        operation_id="CreateMembership",
-        tags=["users"],
-    )
-)
-class GenericMembershipCreate(CreateAPIView):
-    """
-    Create a new generic membership.
-    """
-    permission_classes = (IsVerified, )
-    serializer_class = MembershipCreateSerializer
diff --git a/arkindex/users/serializers.py b/arkindex/users/serializers.py
index 42fb755c08..705ef7d16f 100644
--- a/arkindex/users/serializers.py
+++ b/arkindex/users/serializers.py
@@ -1,18 +1,12 @@
 from django.conf import settings
 from django.contrib.auth.password_validation import validate_password
 from django.contrib.auth.tokens import default_token_generator
-from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
-from django.db import transaction
 from django.utils.http import urlsafe_base64_decode
 from drf_spectacular.utils import extend_schema_field, inline_serializer
 from rest_framework import serializers
-from rest_framework.exceptions import PermissionDenied
 
-from arkindex.process.models import Worker
-from arkindex.project.mixins import WorkerACLMixin
-from arkindex.users.models import Group, Right, Role, User
-from arkindex.users.utils import RightContent, get_max_level
+from arkindex.users.models import User
 
 
 def validate_user_password(user, data):
@@ -171,197 +165,3 @@ class JobSerializer(serializers.Serializer):
         but the enum is just a plain object and not an Enum for Py2 compatibility.
         """
         return instance.get_status(refresh=False)
-
-
-class SimpleGroupSerializer(serializers.ModelSerializer):
-    """
-    A light group serializer with its members count
-    """
-    members_count = serializers.IntegerField(default=None, read_only=True)
-
-    class Meta:
-        model = Group
-        read_only_fields = ("id", "members_count")
-        fields = (
-            "id",
-            "name",
-            "public",
-            "members_count"
-        )
-
-
-class GroupSerializer(SimpleGroupSerializer):
-    """
-    A group serializer with the request member level
-    """
-    level = serializers.IntegerField(default=None, read_only=True)
-
-    class Meta(SimpleGroupSerializer.Meta):
-        fields = SimpleGroupSerializer.Meta.fields + ("level", )
-
-    @transaction.atomic
-    def create(self, validated_data):
-        group = super().create(validated_data)
-        # Associate the creator to the group
-        group.add_member(self.context["request"].user, Role.Admin.value)
-        # Manually set fields required by the serializer
-        group.members_count = 1
-        group.level = Role.Admin.value
-        return group
-
-
-class MembershipSerializer(serializers.ModelSerializer):
-    """
-    Simple serializer for a membership
-    """
-
-    class Meta:
-        model = Right
-        fields = (
-            "id",
-            "level",
-        )
-
-    def validate(self, data):
-        data = super().validate(data)
-        level_field = data.get("level")
-        if not self.instance or not level_field:
-            return data
-        # Assert an update will not remove the last group admin except for a worker
-        if (self.instance.level >= Role.Admin.value
-                and level_field < Role.Admin.value
-                and self.instance.content_type != ContentType.objects.get_for_model(Worker)
-                and self.instance.content_object.memberships.filter(level__gte=Role.Admin.value).count() < 2):
-            raise ValidationError({"level": ["Removing all memberships with an admin privilege is not possible."]})
-        return data
-
-
-class MemberGroupSerializer(MembershipSerializer):
-    """
-    Serialize a group for a specific member with its privilege level
-    """
-    group = SimpleGroupSerializer(source="content_object")
-
-    class Meta(MembershipSerializer.Meta):
-        fields = MembershipSerializer.Meta.fields + ("group", )
-
-    def __init__(self, instance=None, *args, **kwargs):
-        if instance and isinstance(instance, list):
-            # Annotate each target group with the memberships count
-            for right in instance:
-                right.content_object.members_count = right.members_count
-        super().__init__(instance, *args, **kwargs)
-
-
-class GenericMembershipSerializer(MembershipSerializer):
-    """
-    Serialize a generic membership
-    """
-    user = SimpleUserSerializer(allow_null=True)
-    group = SimpleGroupSerializer(allow_null=True)
-
-    class Meta(MembershipSerializer.Meta):
-        fields = MembershipSerializer.Meta.fields + ("user", "group")
-
-
-class MembershipContentType(serializers.ChoiceField):
-    """
-    Specific field to handle a membership generic content
-    """
-
-    def __init__(self, *args, **kwargs):
-        choices = [c.name for c in RightContent]
-        super().__init__(choices, *args, **kwargs)
-
-    def to_representation(self, obj):
-        if not isinstance(obj, ContentType):
-            return None
-        model = obj.model_class()
-        if model not in [c.value for c in RightContent]:
-            return None
-        return RightContent(model).name
-
-    def to_internal_value(self, data):
-        try:
-            return ContentType.objects.get_for_model(RightContent[data].value)
-        except Exception:
-            raise ValidationError([f"'{data}' is not a valid content type."])
-
-
-class MembershipCreateSerializer(MembershipSerializer):
-    """
-    Create a new generic membership
-    """
-    user_email = serializers.EmailField(source="user.email", required=False, allow_null=True)
-    group_id = serializers.UUIDField(required=False, allow_null=True)
-    content_type = MembershipContentType()
-    content_id = serializers.UUIDField(help_text="The ID of the right target.")
-
-    class Meta(MembershipSerializer.Meta):
-        fields = (
-            "id",
-            "level",
-            "user_email",
-            "group_id",
-            "content_type",
-            "content_id"
-        )
-
-    def validate(self, data):
-        data = super().validate(data)
-
-        user_email = data.pop("user", {"email": None})["email"]
-        group_id = data.pop("group_id", None)
-
-        # Assert one of user/group identifier is defined
-        if not (user_email is None) ^ (group_id is None):
-            raise ValidationError({"detail": ["Exactly one of those fields must be defined: user_email, group_id"]})
-
-        # Retrieve the right generic content object
-        content_type = data.pop("content_type")
-        content_model = content_type.model_class()
-        content_id = data.pop("content_id")
-        try:
-            data["content_object"] = content_model.objects.get(id=content_id)
-        except content_model.DoesNotExist:
-            raise ValidationError({"content_id": [f"{content_type.name.capitalize()} with this ID could not be found."]})
-
-        # This is required to use workers ACL mixin out of an API view
-        request_user = self.context["request"].user
-
-        # Retrieve access level
-        access_level = None
-        if content_model is Worker:
-            # Use WorkerACLMixin method as the worker access level may be inherited from its repository
-            access_level = WorkerACLMixin(user=request_user).get_max_level(data["content_object"])
-        else:
-            access_level = get_max_level(request_user, data["content_object"])
-        # Assert the request user a an admin access to the content object
-        if not access_level:
-            raise PermissionDenied(detail="You are not a member of the target content object.")
-        if not access_level or access_level < Role.Admin.value:
-            raise ValidationError({"content_id": ["Only members with an admin privilege are allowed to add other members."]})
-
-        # Special restriction for group memberships
-        if content_model is Group and group_id is not None:
-            raise ValidationError({"group_id": ["It is not possible to create a membership between two groups."]})
-
-        # Retrieve owner of the right
-        owner_data = {}
-        if user_email:
-            try:
-                owner_data["user"] = User.objects.get(email=user_email)
-            except User.DoesNotExist:
-                raise ValidationError({"user_email": ["No user matching this email could be found."]})
-        elif group_id:
-            try:
-                owner_data["group"] = Group.objects.get(id=group_id)
-            except Group.DoesNotExist:
-                raise ValidationError({"group_id": ["No group matching this id could be found."]})
-
-        # Handle existing memberships
-        if data["content_object"].memberships.filter(**owner_data).exists():
-            raise ValidationError({"__all__": ["This membership already exists."]})
-
-        data.update(owner_data)
-        return data
diff --git a/arkindex/users/tests/test_generic_memberships.py b/arkindex/users/tests/test_generic_memberships.py
deleted file mode 100644
index 9371345afe..0000000000
--- a/arkindex/users/tests/test_generic_memberships.py
+++ /dev/null
@@ -1,1078 +0,0 @@
-import uuid
-
-from django.db import IntegrityError
-from django.urls import reverse
-from rest_framework import status
-
-from arkindex.documents.models import Corpus
-from arkindex.process.models import Repository, Worker
-from arkindex.project.tests import FixtureAPITestCase
-from arkindex.users.models import Group, Right, Role, User
-
-
-class TestMembership(FixtureAPITestCase):
-    """
-    Test generic rights management methods
-    """
-
-    @classmethod
-    def setUpTestData(cls):
-        super().setUpTestData()
-        cls.unverified = User.objects.create_user("user@address.com", "P4$5w0Rd")
-        cls.admin = User.objects.create_user("admin@address.com", "P4$5w0Rd")
-        cls.non_admin = User.objects.filter(
-            rights__group_target=cls.group,
-            rights__level=Role.Contributor.value
-        ).first()
-        cls.non_admin.verified_email = True
-        cls.non_admin.save()
-
-        cls.admin_group = Group.objects.create(name="Admin group", public=True)
-        cls.admin_group.add_member(user=cls.admin, level=Role.Admin.value)
-
-        cls.membership = cls.group.memberships.get(user=cls.user)
-        cls.admin_membership = cls.admin_group.memberships.get(user=cls.admin)
-
-    def test_verified_user_group_create(self):
-        """
-        Anonymous and non verified users should not be able to create a group
-        """
-        self.client.force_login(self.unverified)
-        with self.assertNumQueries(2):
-            response = self.client.post(reverse("api:groups-create"), {"name": "new group"})
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.client.logout()
-        with self.assertNumQueries(0):
-            response = self.client.post(reverse("api:groups-create"), {"name": "new group"})
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_create_group_wrong_fields(self):
-        checks = (
-            ({"name": 100 * "A"}, {"name": ["Ensure this field has no more than 64 characters."]}),
-            ({"name": ""}, {"name": ["This field may not be blank."]})
-        )
-        self.client.force_login(self.user)
-        for payload, error in checks:
-            response = self.client.post(reverse("api:groups-create"), payload)
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-            self.assertEqual(response.json(), error)
-
-    def test_create_group(self):
-        """
-        A user creating a group is granted a membership with the maximum privileges level
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(7):
-            response = self.client.post(reverse("api:groups-create"), {"name": "new group", "public": True})
-        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
-        group = Group.objects.get(name="new group")
-        self.assertEqual(response.json(), {
-            "id": str(group.id),
-            "name": "new group",
-            "public": True,
-            "members_count": 1,
-            "level": 100
-        })
-        self.assertCountEqual(
-            group.memberships.values_list("user", "level"),
-            [(self.user.id, Role.Admin.value)]
-        )
-
-    def test_crate_group_duplicated_name(self):
-        """
-        Assert multiple groups can have a similar name
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(7):
-            response = self.client.post(reverse("api:groups-create"), {"name": self.group.name})
-        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
-    def test_list_members_requires_member_user(self):
-        """
-        Only users that belong to a group have the ability to list members, otherwise we return a 403
-        """
-        private_group = Group.objects.create(name="Admin group", public=False)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:memberships-list"), {"group": str(private_group.id)})
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_list_members_superuser(self):
-        """
-        A superadmin is able to list members of any group
-        """
-        private_group = Group.objects.create(name="Private group", public=False)
-        member = private_group.memberships.create(user=self.user, level=Role.Admin.value)
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(6):
-            response = self.client.get(reverse("api:memberships-list"), {"group": str(private_group.id)})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "count": 1,
-            "next": None,
-            "number": 1,
-            "previous": None,
-            "results": [{
-                "id": str(member.id),
-                "level": Role.Admin.value,
-                "user": {
-                    "display_name": self.user.display_name,
-                    "email": self.user.email,
-                    "id": self.user.id,
-                },
-                "group": None,
-            }]
-        })
-
-    def test_list_members_invalid_uuid(self):
-        self.client.force_login(self.user)
-        with self.assertNumQueries(2):
-            response = self.client.get(reverse("api:memberships-list"), {"group": "A"})
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(response.json(), {"group": ["'A' is not a valid UUID."]})
-
-    def test_list_members_wrong_parameters(self):
-        """
-        Exactly one of valid content types should be provided as an URL parameter
-        """
-        cases = (
-            {},
-            {"corpus": str(self.corpus.id), "group": str(self.group.id)},
-            {"non_existing_content_type": str(self.corpus.id)}
-        )
-        self.client.force_login(self.user)
-        for wrong_params in cases:
-            response = self.client.get(reverse("api:memberships-list"), wrong_params)
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-            self.assertDictEqual(response.json(), {
-                "__all__": ["Exactly one of those query parameters must be defined: corpus, repository, group, worker, model, farm."]
-            })
-
-    def test_list_members_public_content(self):
-        """
-        Any user may be able to list all members on a public content (except for groups in the test above)
-        """
-        corpus = Corpus.objects.create(name="Public corpus", public=True)
-        user_right = Right.objects.create(user=self.user, content_object=corpus, level=42)
-        group_right = Right.objects.create(group=self.group, content_object=corpus, level=77)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(8):
-            response = self.client.get(reverse("api:memberships-list"), {"corpus": str(corpus.id)})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "count": 2,
-            "next": None,
-            "number": 1,
-            "previous": None,
-            "results": [
-                {
-                    "id": str(user_right.id),
-                    "level": 42,
-                    "user": {
-                        "display_name": self.user.display_name,
-                        "email": self.user.email,
-                        "id": self.user.id,
-                    },
-                    "group": None,
-                },
-                {
-                    "id": str(group_right.id),
-                    "level": 77,
-                    "user": None,
-                    "group": {
-                        "id": str(self.group.id),
-                        "members_count": self.group.memberships.count(),
-                        "name": self.group.name,
-                        "public": self.group.public,
-                    },
-                }
-            ]
-        })
-
-    def test_list_members_public_group(self):
-        """
-        Any user may not be able to list all members on a public group
-        This behavior is specific to groups
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:memberships-list"), {"group": str(self.admin_group.id)})
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {
-            "detail": "You do not have the required access level to list members for this content."
-        })
-
-    def test_list_members_level_right(self):
-        """
-        List members of a group with their right corresponding to privileges level
-        Members are ordered by display name
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(8):
-            response = self.client.get(reverse("api:memberships-list"), {"group": str(self.group.id)})
-        read_member, write_member, admin_member = self.group.memberships.order_by("level")
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.json(), {
-            "count": 3,
-            "next": None,
-            "number": 1,
-            "previous": None,
-            "results": [
-                {
-                    "id": str(admin_member.id),
-                    "level": Role.Admin.value,
-                    "user": {
-                        "display_name": "Test user",
-                        "email": admin_member.user.email,
-                        "id": admin_member.user.id,
-                    },
-                    "group": None,
-                }, {
-                    "id": str(read_member.id),
-                    "level": read_member.level,
-                    "user": {
-                        "display_name": "Test user read",
-                        "email": read_member.user.email,
-                        "id": read_member.user.id,
-                    },
-                    "group": None,
-                }, {
-                    "id": str(write_member.id),
-                    "level": write_member.level,
-                    "user": {
-                        "display_name": "Test user write",
-                        "email": write_member.user.email,
-                        "id": write_member.user.id,
-                    },
-                    "group": None,
-                }
-            ]
-        })
-
-    def test_list_members_owner_type_filter(self):
-        """
-        Only list group owners of a specific repository
-        """
-        repo = Repository.objects.create(url="http://repo1")
-        Right.objects.bulk_create([
-            Right(user=self.user, content_object=repo, level=42),
-            Right(user=self.non_admin, content_object=repo, level=43),
-            Right(user=self.admin, content_object=repo, level=44),
-            Right(group=self.group, content_object=repo, level=100),
-            Right(group=self.admin_group, content_object=repo, level=24),
-        ])
-        self.client.force_login(self.user)
-        with self.assertNumQueries(8):
-            response = self.client.get(reverse("api:memberships-list"), {"repository": str(repo.id), "type": "group"})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        data = response.json()
-        self.assertEqual(data["count"], 2)
-        self.assertCountEqual(
-            [membership["group"] for membership in data["results"]],
-            [
-                {
-                    "id": str(self.group.id),
-                    "name": "User group",
-                    "public": False,
-                    "members_count": self.group.memberships.count()
-                }, {
-                    "id": str(self.admin_group.id),
-                    "name": "Admin group",
-                    "public": True,
-                    "members_count": self.admin_group.memberships.count()
-                }
-            ]
-        )
-
-    def test_list_worker_members_via_repo_right(self):
-        """
-        A user is able to list members of a worker if he is a member of its repository
-        """
-        repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
-        worker = repo.workers.get(slug="reco")
-        repo.memberships.create(user=self.user, level=Role.Guest.value)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(9):
-            response = self.client.get(reverse("api:memberships-list"), {"worker": str(worker.id)})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        data = response.json()
-        # A worker may have no member
-        self.assertEqual(data["count"], 0)
-
-    def test_retrieve_group_wrong_id(self):
-        self.client.force_login(self.user)
-        with self.assertNumQueries(4):
-            response = self.client.get(reverse("api:group-details", kwargs={"pk": str(uuid.uuid4())}))
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
-    def test_retrieve_group_no_member(self):
-        """
-        A non member of a group may not retrieve its details, even if it is public
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:group-details", kwargs={"pk": str(self.admin_group.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_retrieve_group_details(self):
-        self.client.force_login(self.user)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:group-details", kwargs={"pk": str(self.group.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "id": str(self.group.id),
-            "members_count": 3,
-            "name": self.group.name,
-            "public": self.group.public,
-            "level": Role.Admin.value
-        })
-
-    def test_retrieve_group_superuser(self):
-        """
-        A superuser is allowed to retrieve any group. Its level is always 100
-        """
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(4):
-            response = self.client.get(reverse("api:group-details", kwargs={"pk": str(self.admin_group.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "id": str(self.admin_group.id),
-            "members_count": self.admin_group.memberships.count(),
-            "name": self.admin_group.name,
-            "public": self.admin_group.public,
-            "level": Role.Admin.value
-        })
-
-    def test_update_group_no_admin(self):
-        """
-        User must be a group administrator to edit its properties
-        """
-        self.client.force_login(self.non_admin)
-        with self.assertNumQueries(5):
-            response = self.client.patch(reverse("api:group-details", kwargs={"pk": str(self.group.id)}), {"name": "A"})
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {
-            "detail": "Only members with an admin privilege may update or delete this group."
-        })
-
-    def test_update_group(self):
-        """
-        Group name and public attributes may be updated by an admin member
-        """
-        payload = {
-            "name": "Renamed group",
-            "public": True,
-            "id": uuid.uuid4(),
-            "members_count": 42,
-            "level": 42
-        }
-        self.client.force_login(self.user)
-        with self.assertNumQueries(6):
-            response = self.client.put(
-                reverse("api:group-details", kwargs={"pk": str(self.group.id)}),
-                payload
-            )
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "id": str(self.group.id),
-            "members_count": 3,
-            "name": "Renamed group",
-            "public": True,
-            "level": Role.Admin.value
-        })
-
-    def test_update_group_superuser(self):
-        """
-        A superuser is allowed to retrieve any group. Its level is always 100
-        """
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(5):
-            response = self.client.put(
-                reverse("api:group-details", kwargs={"pk": str(self.admin_group.id)}),
-                {"public": False, "name": "I got you"}
-            )
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "id": str(self.admin_group.id),
-            "members_count": self.admin_group.memberships.count(),
-            "name": "I got you",
-            "public": False,
-            "level": Role.Admin.value
-        })
-
-    def test_delete_group_no_admin(self):
-        self.client.force_login(self.non_admin)
-        with self.assertNumQueries(5):
-            response = self.client.delete(reverse("api:group-details", kwargs={"pk": str(self.group.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {
-            "detail": "Only members with an admin privilege may update or delete this group."
-        })
-
-    def test_delete_group(self):
-        """
-        A group admin is allowed to delete the group
-        """
-        group = Group.objects.create(name="Another group")
-        user = User.objects.create(email="user42@test.test", verified_email=True)
-        group.add_member(user=user, level=Role.Admin.value)
-        self.client.force_login(user)
-        with self.assertNumQueries(7):
-            response = self.client.delete(reverse("api:group-details", kwargs={"pk": str(group.id)}))
-        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-        with self.assertRaises(Group.DoesNotExist):
-            group.refresh_from_db()
-        self.assertTrue(User.objects.filter(id=user.id).exists())
-
-    def test_delete_group_superuser(self):
-        """
-        A superuser is allowed to delete an existing group
-        """
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(7):
-            response = self.client.delete(reverse("api:group-details", kwargs={"pk": str(self.admin_group.id)}))
-        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-        with self.assertRaises(Group.DoesNotExist):
-            self.admin_group.refresh_from_db()
-
-    def test_list_user_memberships_requires_login(self):
-        """
-        User must be logged in to list its memberships
-        """
-        with self.assertNumQueries(0):
-            response = self.client.delete(reverse("api:group-details", kwargs={"pk": str(self.group.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertEqual(response.json(), {"detail": "Authentication credentials were not provided."})
-
-    def test_list_user_memberships(self):
-        """
-        List groups for which the user is a member
-        """
-        new_group = Group.objects.create(name="Another group", public=False)
-        new_group_member = new_group.add_member(user=self.user, level=10)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:user-memberships"))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "count": 2,
-            "number": 1,
-            "next": None,
-            "previous": None,
-            "results": [
-                {
-                    "group":
-                        {
-                            "id": str(new_group.id),
-                            "members_count": 1,
-                            "name": "Another group",
-                            "public": False
-                        },
-                    "id": str(new_group_member.id),
-                    "level": new_group_member.level,
-                }, {
-                    "group":
-                        {
-                            "id": str(self.group.id),
-                            "members_count": self.group.memberships.count(),
-                            "name": self.group.name,
-                            "public": self.group.public
-                        },
-                    "id": str(self.membership.id),
-                    "level": self.membership.level,
-                }
-            ]
-        })
-
-    def test_list_user_memberships_superuser(self):
-        """
-        A superuser is able to list all groups. Memberships have no ID nor level
-        """
-        new_group = Group.objects.create(name="Another group", public=False)
-        new_group.add_member(user=self.user, level=10)
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(3):
-            response = self.client.get(reverse("api:user-memberships"))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "count": 3,
-            "number": 1,
-            "next": None,
-            "previous": None,
-            "results": [
-                {
-                    "group": {
-                        "id": str(self.admin_group.id),
-                        "members_count": 1,
-                        "name": "Admin group",
-                        "public": True
-                    },
-                    "id": None,
-                    "level": None,
-                }, {
-                    "group":
-                        {
-                            "id": str(new_group.id),
-                            "members_count": 1,
-                            "name": "Another group",
-                            "public": False
-                        },
-                    "id": None,
-                    "level": None,
-                }, {
-                    "group":
-                        {
-                            "id": str(self.group.id),
-                            "members_count": self.group.memberships.count(),
-                            "name": self.group.name,
-                            "public": self.group.public
-                        },
-                    "id": None,
-                    "level": None,
-                }
-            ]
-        })
-
-    def test_add_member_non_existing_group(self):
-        """
-        Cannot add a member to a non existing group
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(4):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 42,
-                "user_email": self.non_admin.email,
-                "content_type": "group",
-                "content_id": str(uuid.uuid4())
-            })
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            response.json(),
-            {"content_id": ["Group with this ID could not be found."]}
-        )
-
-    def test_add_member_non_member(self):
-        """
-        Raise a 403 in case the user is not a member of the target content
-        """
-        self.client.force_login(self.user)
-        worker = Worker.objects.get(slug="reco")
-        with self.assertNumQueries(9):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 42,
-                "user_email": self.non_admin.email,
-                "content_type": "worker",
-                "content_id": str(worker.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {"detail": "You are not a member of the target content object."})
-
-    def test_add_member_requires_login(self):
-        with self.assertNumQueries(0):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 42,
-                "user_email": self.non_admin.email,
-                "content_type": "group",
-                "content_id": str(self.group.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {"detail": "Authentication credentials were not provided."})
-
-    def test_add_member_requires_verified(self):
-        self.client.force_login(self.unverified)
-        with self.assertNumQueries(2):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 42,
-                "user_email": self.non_admin.email,
-                "content_type": "group",
-                "content_id": str(uuid.uuid4())
-            })
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
-
-    def test_add_member_requires_group_admin(self):
-        """
-        Only group admins have the ability to add a new member
-        Note that the user is already a member but this check is performed after
-        """
-        self.client.force_login(self.non_admin)
-        with self.assertNumQueries(5):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 42,
-                "user_email": self.non_admin.email,
-                "content_type": "group",
-                "content_id": str(self.group.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(response.json(), {
-            "content_id": ["Only members with an admin privilege are allowed to add other members."]
-        })
-
-    def test_add_member_already_member(self):
-        """
-        A member cannot be added twice
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(7):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 10,
-                "user_email": self.non_admin.email,
-                "content_type": "group",
-                "content_id": str(self.group.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(response.json(), {
-            "__all__": ["This membership already exists."]
-        })
-
-    def test_add_member_non_existing_user(self):
-        """
-        Cannot add a non existing user
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(6):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 10,
-                "user_email": "undefined@nowhe.re",
-                "content_type": "group",
-                "content_id": str(self.group.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(response.json(), {
-            "user_email": ["No user matching this email could be found."]
-        })
-
-    def test_add_member_required_fields(self):
-        """
-        Content type, id and level are required fields
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(2):
-            response = self.client.post(reverse("api:membership-create"), {
-                "access": 42,
-                "id": 1337,
-            })
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(response.json(), {
-            "content_id": ["This field is required."],
-            "content_type": ["This field is required."],
-            "level": ["This field is required."]
-        })
-
-    def test_add_member_wrong_content_type(self):
-        self.client.force_login(self.user)
-        with self.assertNumQueries(2):
-            response = self.client.post(reverse("api:membership-create"), {
-                "content_type": "gloubiboulga",
-                "content_id": uuid.uuid4(),
-                "level": 42
-            })
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(response.json(), {
-            "content_type": ["'gloubiboulga' is not a valid content type."]
-        })
-
-    def test_add_member_right_target_xor(self):
-        """
-        Exactly one of group_id and user_email field must be defined
-        """
-        wrong_targets = (
-            {},
-            {"user_email": "aaa@aa.com", "group_id": str(uuid.uuid4())}
-        )
-        self.client.force_login(self.user)
-        for wrong_target in wrong_targets:
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 42,
-                "content_type": "corpus",
-                "content_id": str(self.corpus.id),
-                **wrong_target
-            })
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-            self.assertDictEqual(response.json(), {
-                "detail": ["Exactly one of those fields must be defined: user_email, group_id"]
-            })
-
-    def test_add_member_level_validation(self):
-        """
-        Level should be an integer between 0 and 100 (included)
-        """
-        checks = (
-            (101, {"level": ["Ensure this value is less than or equal to 100."]}),
-            (-1, {"level": ["Ensure this value is greater than or equal to 1."]})
-        )
-        self.client.force_login(self.user)
-        for level, error in checks:
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": level,
-                "user_email": self.admin.email,
-                "content_type": "group",
-                "content_id": str(self.group.id)
-            })
-            self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-            self.assertDictEqual(response.json(), error)
-
-    def test_add_member_by_email(self):
-        """
-        Adds a new member referenced by its email
-        """
-        user = User.objects.create_user("test@test.de", "Pa$$w0rd")
-        self.client.force_login(self.user)
-        with self.assertNumQueries(8):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 10,
-                "user_email": user.email,
-                "content_type": "group",
-                "content_id": str(self.group.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        membership = user.rights.get(group_target=self.group)
-        self.assertDictEqual(response.json(), {
-            "id": str(membership.id),
-            "content_id": str(self.group.id),
-            "content_type": "group",
-            "level": 10,
-            "user_email": user.email,
-            "group_id": None,
-        })
-
-    def test_add_member_by_email_uppercase_letters(self):
-        """
-        Adds a new member referenced by its email
-        Asserts the endpoint is case insensitive for the email
-        """
-        user = User.objects.create_user("test@test.de", "Pa$$w0rd")
-        self.client.force_login(self.user)
-        with self.assertNumQueries(8):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 10,
-                # Introducing uppercase letters in the user's email
-                "user_email": "tEsT@TeSt.DE",
-                "content_type": "group",
-                "content_id": str(self.group.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        membership = user.rights.get(group_target=self.group)
-        self.assertDictEqual(response.json(), {
-            "id": str(membership.id),
-            "content_id": str(self.group.id),
-            "content_type": "group",
-            "level": 10,
-            "user_email": user.email,
-            "group_id": None,
-        })
-
-    def test_add_member_superuser(self):
-        """
-        A superuser is able to add a member to a groups he has no right on
-        """
-        user = User.objects.create_user("test@test.de", "Pa$$w0rd")
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(7):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 10,
-                "user_email": user.email,
-                "content_type": "group",
-                "content_id": str(self.admin_group.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
-    def test_add_member_via_repo_right(self):
-        """
-        A worker member can inherit the right to add a member from its repository
-        """
-        user = User.objects.create_user("test@test.de", "Pa$$w0rd")
-
-        repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
-        worker = repo.workers.get(slug="reco")
-        repo.memberships.create(user=self.user, level=Role.Admin.value)
-
-        self.client.force_login(self.user)
-        with self.assertNumQueries(11):
-            response = self.client.post(reverse("api:membership-create"), {
-                "level": 10,
-                "user_email": user.email,
-                "content_type": "worker",
-                "content_id": str(worker.id)
-            })
-        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
-    def test_retrieve_membership_not_found(self):
-        """
-        Only memberships on groups the user has a read access can be retrieved
-        """
-        hidden_group = Group.objects.create(name="Hidden group", public=False)
-        hidden_member = hidden_group.add_member(user=self.admin, level=Role.Guest.value)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:membership-details", kwargs={"pk": str(hidden_member.id)}))
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
-    def test_retrieve_membership_details(self):
-        """
-        Any group member can retrieve a specific membership
-        """
-        self.client.force_login(self.non_admin)
-        with self.assertNumQueries(6):
-            response = self.client.get(reverse("api:membership-details", kwargs={"pk": str(self.membership.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "id": str(self.membership.id),
-            "level": Role.Admin.value
-        })
-
-    def test_retrieve_membership_details_superuser(self):
-        """
-        Any group member can retrieve a specific membership
-        """
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(5):
-            response = self.client.get(reverse("api:membership-details", kwargs={"pk": str(self.membership.id)}))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-    def test_edit_membership_requires_verified(self):
-        self.client.force_login(self.unverified)
-        with self.assertNumQueries(2):
-            response = self.client.patch(reverse("api:membership-details", kwargs={"pk": str(uuid.uuid4())}), {})
-        self.assertDictEqual(response.json(), {"detail": "You do not have permission to perform this action."})
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-    def test_edit_membership_requires_admin(self):
-        """
-        The request user has to be an admin of the target member group to edit its membership
-        """
-        self.client.force_login(self.non_admin)
-        with self.assertNumQueries(6):
-            response = self.client.patch(reverse("api:membership-details", kwargs={"pk": str(self.membership.id)}), {})
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(response.json(), {
-            "detail": "Only admins of the target membership group can perform this action."
-        })
-
-    def test_edit_membership_last_admin(self):
-        """
-        At least one admin should be present in the group on a member edition
-        """
-        self.client.force_login(self.user)
-        with self.assertNumQueries(8):
-            response = self.client.patch(
-                reverse("api:membership-details", kwargs={"pk": str(self.membership.id)}),
-                {"level": 1}
-            )
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            response.json(),
-            {"level": ["Removing all memberships with an admin privilege is not possible."]}
-        )
-
-    def test_edit_membership_last_admin_workers(self):
-        """
-        Workers may specially have no member as they depends on a repository
-        """
-        repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
-        worker = repo.workers.get(slug="reco")
-        membership = worker.memberships.create(user=self.user, level=Role.Admin.value)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(10):
-            response = self.client.patch(
-                reverse("api:membership-details", kwargs={"pk": str(membership.id)}),
-                {"level": Role.Guest.value}
-            )
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        # Only one guest right exists on the worker
-        self.assertEqual([m.level for m in worker.memberships.all()], [Role.Guest.value])
-
-    def test_edit_membership_wrong_fields(self):
-        self.non_admin.rights.update(level=Role.Admin.value)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(6):
-            response = self.client.patch(
-                reverse("api:membership-details", kwargs={"pk": str(self.membership.id)}),
-                {"level": 101}
-            )
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(response.json(), {
-            "level": ["Ensure this value is less than or equal to 100."]
-        })
-
-    def test_edit_membership(self):
-        """
-        Only level field may be edited
-        """
-        self.client.force_login(self.user)
-        non_admin_member = self.non_admin.rights.get(group_target=self.group)
-        with self.assertNumQueries(6):
-            response = self.client.patch(
-                reverse("api:membership-details", kwargs={"pk": str(non_admin_member.id)}),
-                {"level": 11, "id": uuid.uuid4(), "content_type": "A", "content_id": uuid.uuid4()}
-            )
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertDictEqual(response.json(), {
-            "id": str(non_admin_member.id),
-            "level": 11
-        })
-
-    def test_edit_privilege_escalation(self):
-        """
-        A user cannot update its own access level if he has no admin access
-        """
-        second_admin = User.objects.create(email="another_user@test.fr", verified_email=True)
-        self.group.memberships.create(user=second_admin, level=Role.Admin.value)
-        cases = [
-            (self.non_admin, Role.Guest.value, status.HTTP_403_FORBIDDEN),
-            (self.non_admin, Role.Admin.value, status.HTTP_403_FORBIDDEN),
-            (second_admin, Role.Guest.value, status.HTTP_200_OK),
-        ]
-        for user, level, response_status in cases:
-            self.client.force_login(user)
-            membership = self.group.memberships.get(user=user)
-            response = self.client.put(
-                reverse("api:membership-details", kwargs={"pk": str(membership.id)}),
-                {"level": level}
-            )
-            self.assertEqual(response.status_code, response_status)
-
-    def test_edit_membership_superadmin(self):
-        """
-        Superadmins are allowed to update any membership level
-        """
-        new_member = self.admin_group.add_member(user=self.user, level=10)
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(5):
-            response = self.client.patch(
-                reverse("api:membership-details", kwargs={"pk": str(new_member.id)}), {"level": 42}
-            )
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        new_member.refresh_from_db()
-        self.assertEqual(new_member.level, 42)
-
-    def test_edit_member_via_repo_right(self):
-        """
-        A worker member can inherit the right to add a member from its repository to edit a worker member
-        """
-        repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
-        worker = repo.workers.get(slug="reco")
-        repo.memberships.create(user=self.user, level=Role.Admin.value)
-
-        new_user = User.objects.create_user("test@test.de", "Pa$$w0rd")
-        new_member = worker.memberships.create(user=new_user, level=Role.Guest.value)
-
-        self.client.force_login(self.user)
-        with self.assertNumQueries(9):
-            response = self.client.patch(reverse("api:membership-details", kwargs={"pk": str(new_member.id)}), {
-                "level": Role.Admin.value,
-            })
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        new_member.refresh_from_db()
-        self.assertEqual(new_member.level, Role.Admin.value)
-
-    def test_delete_membership_last_admin(self):
-        """
-        At least one admin should be present for the corresponding object on a member deletion
-        """
-        # The user is the only admin for this corpus
-        corpus_membership = self.corpus.memberships.get(user=self.user)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(8):
-            response = self.client.delete(reverse("api:membership-details", kwargs={"pk": str(corpus_membership.id)}))
-        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-        self.assertDictEqual(
-            response.json(),
-            {"detail": ["Removing all memberships with an admin privilege is not possible."]}
-        )
-
-    def test_delete_membership_remaining_admins(self):
-        """
-        It is possible to downgrade an admin privilege if there are other admins for the corresponding object
-        """
-        admin_group = Group.objects.create(name="Another group")
-        # We add a group as an admin of the user corpus
-        group_membership = admin_group.rights.create(content_object=self.corpus, level=Role.Admin.value)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(9):
-            response = self.client.delete(reverse("api:membership-details", kwargs={"pk": str(group_membership.id)}))
-        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-        self.assertEqual(admin_group.rights.count(), 0)
-
-    def test_delete_membership_last_admin_workers(self):
-        """
-        Workers may have no member as they depends on a repository
-        """
-        repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
-        worker = repo.workers.get(slug="reco")
-        membership = worker.memberships.create(user=self.user, level=Role.Admin.value)
-        self.client.force_login(self.user)
-        with self.assertNumQueries(10):
-            response = self.client.delete(
-                reverse("api:membership-details", kwargs={"pk": str(membership.id)}),
-                {"level": Role.Guest.value}
-            )
-        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-        # Only one guest right exists on the worker
-        self.assertEqual(worker.memberships.count(), 0)
-
-    def test_delete_membership_non_admin(self):
-        """
-        Non admin members are not allowed to remove another member
-        """
-        self.client.force_login(self.non_admin)
-        with self.assertNumQueries(6):
-            response = self.client.delete(reverse("api:membership-details", kwargs={"pk": str(self.membership.id)}))
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertDictEqual(
-            response.json(),
-            {"detail": "Only admins of the target membership group can perform this action."}
-        )
-
-    def test_delete_membership_superuser(self):
-        """
-        Superadmins are allowed to delete any existing membership
-        """
-        new_member = self.admin_group.add_member(user=self.user, level=10)
-        self.client.force_login(self.superuser)
-        with self.assertNumQueries(6):
-            response = self.client.delete(reverse("api:membership-details", kwargs={"pk": str(new_member.id)}))
-        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-
-    def test_delete_own_membership(self):
-        """
-        Any member is able to remove its own membership
-        """
-        non_admin_membership = self.group.memberships.get(user=self.non_admin)
-        self.client.force_login(self.non_admin)
-        with self.assertNumQueries(7):
-            response = self.client.delete(reverse("api:membership-details", kwargs={"pk": str(non_admin_membership.id)}))
-        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-        with self.assertRaises(Right.DoesNotExist):
-            non_admin_membership.refresh_from_db()
-
-    def test_delete_member_via_repo_right(self):
-        """
-        A worker member can inherit the right to add a member from its repository to remove a worker member
-        """
-        repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
-        worker = repo.workers.get(slug="reco")
-        repo.memberships.create(user=self.user, level=Role.Admin.value)
-
-        new_user = User.objects.create_user("test@test.de", "Pa$$w0rd")
-        new_member = worker.memberships.create(user=new_user, level=Role.Guest.value)
-
-        self.client.force_login(self.user)
-        with self.assertNumQueries(9):
-            response = self.client.delete(reverse("api:membership-details", kwargs={"pk": str(new_member.id)}))
-        self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-        with self.assertRaises(Right.DoesNotExist):
-            new_member.refresh_from_db()
-
-    def test_right_unique_user(self):
-        repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
-        repo.memberships.create(user=self.user, level=Role.Admin.value)
-        with self.assertRaises(IntegrityError):
-            repo.memberships.create(user=self.user, level=Role.Admin.value)
-
-    def test_right_unique_group(self):
-        repo = Repository.objects.get(url="http://my_repo.fake/workers/worker")
-        repo.memberships.create(group=self.group, level=Role.Admin.value)
-        with self.assertRaises(IntegrityError):
-            repo.memberships.create(group=self.group, level=Role.Admin.value)
diff --git a/arkindex/users/tests/test_manager_acl.py b/arkindex/users/tests/test_manager_acl.py
deleted file mode 100644
index fe7665abb5..0000000000
--- a/arkindex/users/tests/test_manager_acl.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from django.contrib.auth.models import AnonymousUser
-from django.test import TestCase
-
-from arkindex.documents.models import Corpus
-from arkindex.users.models import Role, User
-
-
-class TestACL(TestCase):
-    """
-    Test Corpus manager ACL helpers
-    """
-
-    @classmethod
-    def setUpTestData(cls):
-        super().setUpTestData()
-        cls.anon = AnonymousUser()
-        cls.user = User.objects.create_user("user@address.com", "P4$5w0Rd")
-        cls.admin = User.objects.create_superuser("admin@address.com", "P4$5w0Rd")
-
-        cls.corpus_public = Corpus.objects.create(name="A Public", public=True)
-        cls.corpus_private = Corpus.objects.create(name="B Private")
-        cls.corpus_private.memberships.create(user=cls.user, level=Role.Guest.value)
-        cls.corpus_hidden = Corpus.objects.create(name="C Hidden")
-
-    def test_anon(self):
-        # An anonymous user has only access to public
-        self.assertCountEqual(
-            Corpus.objects.readable(self.anon).values_list("id", flat=True),
-            [self.corpus_public.id]
-        )
-
-    def test_user(self):
-        # An anonymous user has access to public & private
-        self.assertFalse(self.user.is_admin)
-        self.assertCountEqual(
-            Corpus.objects.readable(self.user).values_list("id", flat=True),
-            [
-                self.corpus_private.id,
-                *Corpus.objects.filter(public=True).values_list("id", flat=True)
-            ]
-        )
-
-    def test_admin(self):
-        # An admin has access with admin privileges to all corpora
-        self.assertTrue(self.admin.is_admin)
-        self.assertCountEqual(
-            Corpus.objects.admin(self.admin).values_list("id"),
-            Corpus.objects.all().values_list("id")
-        )
diff --git a/arkindex/users/tests/test_registration.py b/arkindex/users/tests/test_registration.py
index f0f9e6206e..7376532c48 100644
--- a/arkindex/users/tests/test_registration.py
+++ b/arkindex/users/tests/test_registration.py
@@ -1,4 +1,5 @@
 import urllib.parse
+from unittest.mock import call, patch
 
 from django.contrib import auth
 from django.contrib.auth.tokens import default_token_generator
@@ -117,7 +118,8 @@ class TestRegistration(FixtureAPITestCase):
         self.assertTrue(auth.get_user(self.client).is_authenticated)
         self.assertEqual(auth.get_user(self.client).email, "email@address.com")
 
-    def test_email_verification(self):
+    @patch("arkindex.users.managers.BaseACLManager.filter_rights", return_value=Corpus.objects.none())
+    def test_email_verification(self, filter_rights_mock):
         newuser = User.objects.create_user("newuser@example.com", password="hunter2")
         self.assertFalse(newuser.verified_email)
         self.assertTrue(newuser.has_usable_password())
@@ -135,18 +137,19 @@ class TestRegistration(FixtureAPITestCase):
         newuser.refresh_from_db()
         self.assertTrue(newuser.verified_email)
 
+        self.assertEqual(filter_rights_mock.call_count, 1)
+        self.assertEqual(filter_rights_mock.call_args, call(newuser, Corpus, Role.Admin.value))
+
         # Assert a trial corpus is created for the new user
-        new_corpus = Corpus.objects.exclude(id__in=corpora)
-        self.assertEqual(new_corpus.count(), 1)
-        self.assertEqual(new_corpus.get().name, "My Project")
-        self.assertEqual(new_corpus.get().description, "Project for newuser@example.com")
-        self.assertCountEqual(
-            list(Corpus.objects.admin(newuser)),
-            list(new_corpus)
-        )
+        new_corpus = Corpus.objects.exclude(id__in=corpora).get()
+        self.assertEqual(new_corpus.name, "My Project")
+        self.assertEqual(new_corpus.description, "Project for newuser@example.com")
+        membership = new_corpus.memberships.get()
+        self.assertEqual(membership.user, newuser)
+        self.assertEqual(membership.level, Role.Admin.value)
         # Assert defaults types are set on the new corpus
         self.assertCountEqual(
-            list(new_corpus.get().types.values(
+            list(new_corpus.types.values(
                 "slug",
                 "display_name",
                 "folder",
@@ -172,7 +175,7 @@ class TestRegistration(FixtureAPITestCase):
         newuser = User.objects.create_user("newuser@example.com", password="hunter2")
         self.assertFalse(newuser.verified_email)
 
-        with (self.settings(SIGNUP_DEFAULT_GROUP=str(test_group.id)), self.assertNumQueries(19)):
+        with (self.settings(SIGNUP_DEFAULT_GROUP=str(test_group.id)), self.assertNumQueries(15)):
             response = self.client.get("{}?{}".format(
                 reverse("api:user-token"),
                 urllib.parse.urlencode({
@@ -192,7 +195,7 @@ class TestRegistration(FixtureAPITestCase):
 
         with (
             self.settings(SIGNUP_DEFAULT_GROUP="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
-            self.assertNumQueries(15),
+            self.assertNumQueries(10),
             self.assertRaises(Group.DoesNotExist)
         ):
             self.client.get("{}?{}".format(
diff --git a/arkindex/users/utils.py b/arkindex/users/utils.py
index 201708f558..9fcf143b03 100644
--- a/arkindex/users/utils.py
+++ b/arkindex/users/utils.py
@@ -1,119 +1,8 @@
-from django.db.models import IntegerField, Q, Value, functions
-from django.db.models.query_utils import DeferredAttribute
-from enumfields import Enum
+from django.conf import settings
+from django.utils.module_loading import cached_import
 
-from arkindex.documents.models import Corpus
-from arkindex.ponos.models import Farm
-from arkindex.process.models import Repository, Worker
-from arkindex.training.models import Model
-from arkindex.users.models import Group, Role
+_filter_module = getattr(settings, "RIGHTS_FILTER_MODULE", None) or "arkindex.users.allow_all"
 
-PUBLIC_LEVEL = Role.Guest.value
-
-
-class RightContent(Enum):
-    corpus = Corpus
-    repository = Repository
-    group = Group
-    worker = Worker
-    model = Model
-    farm = Farm
-
-
-def has_public_field(model):
-    return isinstance(
-        getattr(model, "public", None),
-        DeferredAttribute
-    )
-
-
-def get_public_instances(model):
-    return model.objects \
-        .filter(public=True) \
-        .annotate(max_level=Value(PUBLIC_LEVEL, IntegerField()))
-
-
-def check_level_param(level):
-    assert isinstance(level, int), "An integer level is required to compare access rights."
-    assert level >= 1, "Level integer should be greater than or equal to 1."
-    assert level <= 100, "level integer should be lower than or equal to 100"
-
-
-def filter_rights(user, model, level):
-    """
-    Return a generic queryset of objects with access rights for this user.
-    Level filtering parameter should be an integer between 1 and 100.
-    """
-    check_level_param(level)
-
-    public = level <= PUBLIC_LEVEL and has_public_field(model)
-
-    # Handle special authentications
-    if user.is_anonymous:
-        # Anonymous users have Guest access on public instances only
-        if not public:
-            return model.objects.none()
-        return get_public_instances(model)
-    elif user.is_admin:
-        # Superusers have an Admin access to all corpora
-        return model.objects.all() \
-            .annotate(max_level=Value(Role.Admin.value, IntegerField()))
-
-    # Filter users rights and annotate the resulting level for those rights
-    queryset = model.objects \
-        .filter(
-            # Filter instances with rights concerning this user
-            Q(memberships__user=user)
-            | Q(memberships__group__memberships__user=user)
-        ) \
-        .annotate(
-            # Keep only the lowest level for each right via group
-            max_level=functions.Least(
-                "memberships__level",
-                # In case of direct right, the group level will be skipped (Null value)
-                "memberships__group__memberships__level"
-            )
-        ) \
-        .filter(max_level__gte=level)
-
-    # Use a join to add public instances as this is the more elegant solution
-    if public:
-        queryset = queryset.union(get_public_instances(model))
-
-    return queryset
-
-
-def get_max_level(user, instance):
-    """
-    Returns the maximum access level on a given instance
-    """
-    default_level = None
-    if getattr(instance, "public", False):
-        default_level = PUBLIC_LEVEL
-
-    # Handle special authentications
-    if user.is_anonymous:
-        return default_level
-    elif user.is_admin:
-        # Superusers have an Admin access to all corpora
-        return Role.Admin.value
-
-    rights = instance.memberships \
-        .filter(
-            # Filter instances with rights concerning this user
-            Q(user=user)
-            | Q(group__memberships__user=user)
-        ) \
-        .annotate(
-            # Keep only the lowest level for each right via group
-            max_level=functions.Least(
-                "level",
-                # In case of direct right, the group level will be skipped (Null value)
-                "group__memberships__level"
-            )
-        )
-
-    # Filter out the right with the maximum level
-    max_right = max(rights, key=lambda r: r.max_level, default=None)
-
-    return max_right and max_right.max_level or default_level
+filter_rights = cached_import(_filter_module, "filter_rights")
+get_max_level = cached_import(_filter_module, "get_max_level")
+has_access = cached_import(_filter_module, "has_access")
diff --git a/requirements.txt b/requirements.txt
index fea699888b..88d51a64c9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,9 @@ django-pgtrigger==4.7.0
 django-rq==2.8.1
 djangorestframework==3.12.4
 djangorestframework-simplejwt==5.2.2
+docker==7.0.0
 drf-spectacular==0.18.2
+python-magic==0.4.27
 python-memcached==1.59
 pytz==2023.3
 PyYAML==6.0
-- 
GitLab