diff --git a/arkindex/documents/api/elements.py b/arkindex/documents/api/elements.py
index 8178827464fc736e98e046cb0fe1e7b5cb24ec8e..4c95fdea0bb91c09e222956edb7f6e36b8787ac8 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 20d3e109a80e64313b5c5424d140a0e90c7460ac..038367ce6f36e56fd72e6dcc0e364cd73700248a 100644
--- a/arkindex/documents/fixtures/data.json
+++ b/arkindex/documents/fixtures/data.json
@@ -1,7 +1,7 @@
 [
 {
     "model": "process.process",
-    "pk": "245bc206-350c-43d5-8db3-d98645c4eaa9",
+    "pk": "111cabac-6f43-4ff3-b6a3-77f79eb7e7fb",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
@@ -13,7 +13,7 @@
         "activity_state": "disabled",
         "started": null,
         "finished": null,
-        "farm": "c19116b6-866f-4bb7-b447-3544629a8151",
+        "farm": "ae9b2873-eca1-4e11-a3e6-9d825ea0882f",
         "element": null,
         "folder_type": null,
         "element_type": null,
@@ -32,19 +32,19 @@
 },
 {
     "model": "process.process",
-    "pk": "64b06ffd-a8c4-4ba2-bbb5-c99eb99121a0",
+    "pk": "1c47429f-a493-4555-bfad-bf8e85509237",
     "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": "local",
         "revision": null,
         "activity_state": "disabled",
         "started": null,
         "finished": null,
-        "farm": "c19116b6-866f-4bb7-b447-3544629a8151",
+        "farm": null,
         "element": null,
         "folder_type": null,
         "element_type": null,
@@ -63,7 +63,7 @@
 },
 {
     "model": "process.process",
-    "pk": "98e30036-bd74-4480-82c0-98c3199a4503",
+    "pk": "285f00ac-5f20-4aad-aee3-e7331f83114e",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
@@ -94,19 +94,19 @@
 },
 {
     "model": "process.process",
-    "pk": "e395a111-ab5a-4c93-8901-b1f8bafde684",
+    "pk": "715a2c6b-a3ef-44be-8928-b63ef762c04d",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "name": null,
-        "creator": 1,
-        "corpus": null,
-        "mode": "local",
+        "name": "Process fixture",
+        "creator": 2,
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "mode": "workers",
         "revision": null,
         "activity_state": "disabled",
         "started": null,
         "finished": null,
-        "farm": null,
+        "farm": "ae9b2873-eca1-4e11-a3e6-9d825ea0882f",
         "element": null,
         "folder_type": null,
         "element_type": null,
@@ -125,182 +125,184 @@
 },
 {
     "model": "process.repository",
-    "pk": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+    "pk": "7fa115ef-b142-4379-a48c-7611fc3c6e64",
     "fields": {
         "url": "http://my_repo.fake/workers/worker"
     }
 },
 {
     "model": "process.repository",
-    "pk": "b44445fb-1586-4673-b61c-c8057ccc9878",
+    "pk": "bd6b6e2b-614f-4cb7-a2c1-681302a0eb35",
     "fields": {
         "url": "http://gitlab/repo"
     }
 },
 {
     "model": "process.revision",
-    "pk": "5f17c014-45e6-416e-9a6e-86a5de0f79c5",
+    "pk": "5826626e-6233-45a1-beb4-437158a12311",
     "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": "7fa115ef-b142-4379-a48c-7611fc3c6e64",
+        "hash": "1337",
+        "message": "My w0rk3r",
+        "author": "Test user"
     }
 },
 {
     "model": "process.revision",
-    "pk": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+    "pk": "b3dfded2-ec79-4d8f-a444-2606fa9c09bb",
     "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": "bd6b6e2b-614f-4cb7-a2c1-681302a0eb35",
+        "hash": "42",
+        "message": "Salve",
+        "author": "Some user"
     }
 },
 {
     "model": "process.worker",
-    "pk": "19c68046-9fde-4aa6-804d-d3888205722a",
+    "pk": "2031fda4-eb0f-4a0e-a356-2173edd71ec2",
     "fields": {
         "name": "Generic worker with a Model",
         "slug": "generic",
-        "type": "189c3b32-1172-4a79-b9a2-3cb14f7f52e3",
+        "type": "692ec457-fb42-4b36-9e92-cbb8786e62c0",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": "7fa115ef-b142-4379-a48c-7611fc3c6e64",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "1ab3c191-f306-4c6b-89b5-5d41a50a3d22",
+    "pk": "7c4d8def-5cfa-4c74-9ccb-ff0e50728381",
     "fields": {
-        "name": "Document layout analyser",
-        "slug": "dla",
-        "type": "c804b4ef-b2ed-4b82-90c6-fb020e3542d8",
+        "name": "Custom worker",
+        "slug": "custom",
+        "type": "aa8bb452-390e-4898-b5d9-3d50799517d3",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": null,
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "5a12340d-c2b9-4d28-800b-6563fb9f6bec",
+    "pk": "8c4476f3-5d3d-4e4f-bcbd-0bb3787b751a",
     "fields": {
-        "name": "Recognizer",
-        "slug": "reco",
-        "type": "189c3b32-1172-4a79-b9a2-3cb14f7f52e3",
+        "name": "Document layout analyser",
+        "slug": "dla",
+        "type": "552c4367-459c-4388-b92b-dcf438c88843",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": "7fa115ef-b142-4379-a48c-7611fc3c6e64",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "62666365-d37d-4e30-91ad-723405dbdc8e",
+    "pk": "9195247f-d052-4ec6-8828-b2dbd94d5626",
     "fields": {
-        "name": "Worker requiring a GPU",
-        "slug": "worker-gpu",
-        "type": "429e7774-99cd-4672-9438-7d576a69a1a4",
+        "name": "File import",
+        "slug": "file_import",
+        "type": "f0d162ed-62a7-47c3-ac7d-8a8a8a733c1e",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": "7fa115ef-b142-4379-a48c-7611fc3c6e64",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "bf76d80a-ca52-47e2-8670-2d60c1a675b0",
+    "pk": "bd6b30f8-4756-47b0-8380-c455caa80802",
     "fields": {
-        "name": "Custom worker",
-        "slug": "custom",
-        "type": "2b2c3dae-98be-42c3-927b-28440f8df5d0",
+        "name": "Recognizer",
+        "slug": "reco",
+        "type": "692ec457-fb42-4b36-9e92-cbb8786e62c0",
         "description": "",
-        "repository": null,
+        "repository": "7fa115ef-b142-4379-a48c-7611fc3c6e64",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.worker",
-    "pk": "e61fde1d-e5bb-4188-aefa-1ceb04fa34a7",
+    "pk": "f0f67f2c-3f02-4d22-b430-092b232eea5a",
     "fields": {
-        "name": "File import",
-        "slug": "file_import",
-        "type": "2164e147-3109-4a68-93d7-d68341dedfb2",
+        "name": "Worker requiring a GPU",
+        "slug": "worker-gpu",
+        "type": "0fc130c1-011c-4352-9859-3053f8525611",
         "description": "",
-        "repository": "6a07d21c-cc43-4c2b-8c72-5105a0399055",
+        "repository": "7fa115ef-b142-4379-a48c-7611fc3c6e64",
         "public": false,
         "archived": null
     }
 },
 {
     "model": "process.workertype",
-    "pk": "189c3b32-1172-4a79-b9a2-3cb14f7f52e3",
+    "pk": "0fc130c1-011c-4352-9859-3053f8525611",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "slug": "recognizer",
-        "display_name": "Recognizer"
+        "slug": "worker",
+        "display_name": "Worker requiring a GPU"
     }
 },
 {
     "model": "process.workertype",
-    "pk": "2164e147-3109-4a68-93d7-d68341dedfb2",
+    "pk": "552c4367-459c-4388-b92b-dcf438c88843",
     "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": "692ec457-fb42-4b36-9e92-cbb8786e62c0",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "slug": "custom",
-        "display_name": "Custom"
+        "slug": "recognizer",
+        "display_name": "Recognizer"
     }
 },
 {
     "model": "process.workertype",
-    "pk": "429e7774-99cd-4672-9438-7d576a69a1a4",
+    "pk": "aa8bb452-390e-4898-b5d9-3d50799517d3",
     "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": "f0d162ed-62a7-47c3-ac7d-8a8a8a733c1e",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "slug": "dla",
-        "display_name": "Document Layout Analysis"
+        "slug": "import",
+        "display_name": "Import"
     }
 },
 {
     "model": "process.workerversion",
-    "pk": "1e0dee90-0973-4017-b23d-4d972d7cfc09",
+    "pk": "39b56fa8-5244-4385-a5fc-12c3d3a8c1eb",
     "fields": {
-        "worker": "e61fde1d-e5bb-4188-aefa-1ceb04fa34a7",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "8c4476f3-5d3d-4e4f-bcbd-0bb3787b751a",
+        "revision": "5826626e-6233-45a1-beb4-437158a12311",
         "version": null,
-        "configuration": {},
+        "configuration": {
+            "test": 42
+        },
         "state": "available",
         "gpu_usage": "disabled",
         "model_usage": "disabled",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "docker_image": "00515030-0750-46ea-9aa5-62c8bcd0252f",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -308,18 +310,16 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "5ce2dcd8-f35e-41d1-bd73-b40c9661db77",
+    "pk": "58751d57-3620-4f19-b7c7-4d579e471d28",
     "fields": {
-        "worker": "bf76d80a-ca52-47e2-8670-2d60c1a675b0",
-        "revision": null,
-        "version": 1,
-        "configuration": {
-            "custom": "value"
-        },
-        "state": "created",
+        "worker": "9195247f-d052-4ec6-8828-b2dbd94d5626",
+        "revision": "5826626e-6233-45a1-beb4-437158a12311",
+        "version": null,
+        "configuration": {},
+        "state": "available",
         "gpu_usage": "disabled",
         "model_usage": "disabled",
-        "docker_image": null,
+        "docker_image": "00515030-0750-46ea-9aa5-62c8bcd0252f",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -327,18 +327,18 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+    "pk": "a4b91c83-650e-4881-a2ac-82d9598f776d",
     "fields": {
-        "worker": "5a12340d-c2b9-4d28-800b-6563fb9f6bec",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "2031fda4-eb0f-4a0e-a356-2173edd71ec2",
+        "revision": "5826626e-6233-45a1-beb4-437158a12311",
         "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": "00515030-0750-46ea-9aa5-62c8bcd0252f",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -346,10 +346,10 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "79d83370-d164-4052-bb29-18f475916ea4",
+    "pk": "c82e0c63-ef75-45c7-b97f-f67927b0453e",
     "fields": {
-        "worker": "62666365-d37d-4e30-91ad-723405dbdc8e",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "f0f67f2c-3f02-4d22-b430-092b232eea5a",
+        "revision": "5826626e-6233-45a1-beb4-437158a12311",
         "version": null,
         "configuration": {
             "test": 42
@@ -357,7 +357,7 @@
         "state": "available",
         "gpu_usage": "required",
         "model_usage": "disabled",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "docker_image": "00515030-0750-46ea-9aa5-62c8bcd0252f",
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -365,18 +365,18 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "9e43ea6a-3f53-4a54-9a66-8f1699570f59",
+    "pk": "d120d2c7-165a-421c-8cb3-ea67452d063d",
     "fields": {
-        "worker": "19c68046-9fde-4aa6-804d-d3888205722a",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
-        "version": null,
+        "worker": "7c4d8def-5cfa-4c74-9ccb-ff0e50728381",
+        "revision": null,
+        "version": 1,
         "configuration": {
-            "test": 42
+            "custom": "value"
         },
-        "state": "available",
+        "state": "created",
         "gpu_usage": "disabled",
-        "model_usage": "required",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "model_usage": "disabled",
+        "docker_image": null,
         "docker_image_iid": null,
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z"
@@ -384,10 +384,10 @@
 },
 {
     "model": "process.workerversion",
-    "pk": "b0db756e-b291-4ffe-99c7-cd347741d7db",
+    "pk": "f50622da-5cdc-406d-9c3d-2416da0100a0",
     "fields": {
-        "worker": "1ab3c191-f306-4c6b-89b5-5d41a50a3d22",
-        "revision": "88be293e-5e58-4b60-8438-3feaf38c0f12",
+        "worker": "bd6b30f8-4756-47b0-8380-c455caa80802",
+        "revision": "5826626e-6233-45a1-beb4-437158a12311",
         "version": null,
         "configuration": {
             "test": 42
@@ -395,7 +395,7 @@
         "state": "available",
         "gpu_usage": "disabled",
         "model_usage": "disabled",
-        "docker_image": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+        "docker_image": "00515030-0750-46ea-9aa5-62c8bcd0252f",
         "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": "296d16dc-b085-4192-9ba0-7690f3ecb1f9",
     "fields": {
-        "process": "64b06ffd-a8c4-4ba2-bbb5-c99eb99121a0",
-        "version": "b0db756e-b291-4ffe-99c7-cd347741d7db",
+        "process": "1c47429f-a493-4555-bfad-bf8e85509237",
+        "version": "d120d2c7-165a-421c-8cb3-ea67452d063d",
         "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": "29e4ecc4-be14-4b57-85bc-7342fa461d89",
     "fields": {
-        "process": "64b06ffd-a8c4-4ba2-bbb5-c99eb99121a0",
-        "version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "process": "285f00ac-5f20-4aad-aee3-e7331f83114e",
+        "version": "d120d2c7-165a-421c-8cb3-ea67452d063d",
         "model_version": null,
-        "parents": "[\"4b931fc3-4c2b-44ea-8743-4907915554bb\"]",
+        "parents": "[]",
         "configuration": null,
-        "summary": "Worker Recognizer @ 70b6b8",
+        "summary": "Worker Custom worker @ version 1",
         "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": "2b67063a-0b6e-4e60-b6c7-4c9fb120a52f",
     "fields": {
-        "process": "e395a111-ab5a-4c93-8901-b1f8bafde684",
-        "version": "5ce2dcd8-f35e-41d1-bd73-b40c9661db77",
+        "process": "715a2c6b-a3ef-44be-8928-b63ef762c04d",
+        "version": "39b56fa8-5244-4385-a5fc-12c3d3a8c1eb",
         "model_version": null,
         "parents": "[]",
         "configuration": null,
-        "summary": "Worker Custom worker @ version 1",
+        "summary": "Worker Document layout analyser @ 39b56f",
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "has_results": false
@@ -448,14 +448,14 @@
 },
 {
     "model": "process.workerrun",
-    "pk": "86240072-39ba-4597-a7a5-9078f3134657",
+    "pk": "8a661430-dac6-4b27-89c3-e597842bf538",
     "fields": {
-        "process": "98e30036-bd74-4480-82c0-98c3199a4503",
-        "version": "5ce2dcd8-f35e-41d1-bd73-b40c9661db77",
+        "process": "715a2c6b-a3ef-44be-8928-b63ef762c04d",
+        "version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "model_version": null,
-        "parents": "[]",
+        "parents": "[\"2b67063a-0b6e-4e60-b6c7-4c9fb120a52f\"]",
         "configuration": null,
-        "summary": "Worker Custom worker @ version 1",
+        "summary": "Worker Recognizer @ f50622",
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "has_results": false
@@ -463,7 +463,7 @@
 },
 {
     "model": "documents.corpus",
-    "pk": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+    "pk": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
     "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": "099df260-b6a6-4559-b091-f2f57a78640f",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "word",
-        "display_name": "Word",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "slug": "page",
+        "display_name": "Page",
         "folder": false,
         "indexable": false,
         "color": "28b62c"
@@ -488,23 +488,23 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "4200ad41-d032-476a-acb4-943e6481ee6d",
+    "pk": "3b0543d9-e822-42aa-90fc-c45af23524e1",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "volume",
-        "display_name": "Volume",
-        "folder": true,
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "slug": "text_line",
+        "display_name": "Line",
+        "folder": false,
         "indexable": false,
         "color": "28b62c"
     }
 },
 {
     "model": "documents.elementtype",
-    "pk": "634e6aee-87a1-4845-a63e-83e7226aa557",
+    "pk": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "text_line",
-        "display_name": "Line",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "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": "71cb6579-1b09-4bc9-9278-48f648fb6af6",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "page",
-        "display_name": "Page",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "slug": "act",
+        "display_name": "Act",
         "folder": false,
         "indexable": false,
         "color": "28b62c"
@@ -524,9 +524,9 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "bc871e02-a23f-4799-a4d7-bb81813f41f5",
+    "pk": "eb4f8afb-4749-49a6-b3ba-d72be2d50384",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
         "slug": "surface",
         "display_name": "Surface",
         "folder": false,
@@ -536,291 +536,291 @@
 },
 {
     "model": "documents.elementtype",
-    "pk": "eda3da9f-1a6b-4f7a-91d1-f28e4e74f76a",
+    "pk": "f9fa0ac3-c3a5-4916-b7a1-18de2de95918",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "slug": "act",
-        "display_name": "Act",
-        "folder": false,
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "slug": "volume",
+        "display_name": "Volume",
+        "folder": true,
         "indexable": false,
         "color": "28b62c"
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "04489822-c85b-4ac8-8d59-f7e1ac914295",
+    "pk": "0e601c72-0bfc-4784-a514-7522515b0cc0",
     "fields": {
-        "element": "6c6a774e-88c3-40c5-9913-d7ec2a3d5125",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
+        "element": "ef4e20a9-7991-4d62-a2e8-bf565c0c6b33",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"46ee25df-c8c1-46f8-af4c-d62d0c8f4600\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "0a801b84-7243-4ee0-857a-446a6af3ae97",
+    "pk": "0f814347-76db-4bcf-9876-584a7294a444",
     "fields": {
-        "element": "3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64",
-        "path": "[]",
-        "ordering": 0
+        "element": "51af1d15-e242-49e4-a797-7697c5162059",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\"]",
+        "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "0aad9073-0d49-492e-8455-4bd99e492e89",
+    "pk": "2c714775-0cd2-4375-b481-67dd7c6e313f",
     "fields": {
-        "element": "469f4606-f5ea-406d-a0bd-6d48050c39ed",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"4d2f59fe-9808-44b1-b3b5-017490ae4064\"]",
+        "element": "fb316906-9851-4851-a3d6-2a82a2b0f066",
+        "path": "[\"67ca764b-de6a-41a8-885c-ce2ce422e391\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "2a801688-b438-4234-a1de-4b06553f1d55",
+    "pk": "2cead33e-466f-4d4c-9c83-05fc3b614382",
     "fields": {
-        "element": "d3dc3f63-9894-434b-9715-7bd906b63e63",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6c6a774e-88c3-40c5-9913-d7ec2a3d5125\"]",
-        "ordering": 0
+        "element": "9b7b7b7a-0b5e-42ed-8cbc-75caf378d45e",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"be338e62-cfcd-4faa-9b23-9da3900313f9\"]",
+        "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "394d7eca-1ccc-47a4-ae44-8dd33c9ceb17",
+    "pk": "2fd6a3e6-b36c-48b3-aea1-1f77ae76add7",
     "fields": {
-        "element": "60313262-3273-4c39-a8ee-cedc733f6990",
-        "path": "[\"7a007e29-b509-4c4e-8321-78464df6f03a\"]",
-        "ordering": 0
+        "element": "d2fd3f5e-30b4-4f80-ad81-62d6cea02e2e",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"51af1d15-e242-49e4-a797-7697c5162059\"]",
+        "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "3d71eb28-24c5-4028-a72b-a3c170c117eb",
+    "pk": "46f31ff0-4569-4824-945f-68972a42221a",
     "fields": {
-        "element": "17a65610-023b-46c7-93a6-453335626d17",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6c6a774e-88c3-40c5-9913-d7ec2a3d5125\"]",
-        "ordering": 3
+        "element": "369a2f84-b6fc-48f4-8a8f-f5261674329c",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"2cdfa4f2-a66c-46c0-8038-8e5ac7e7d36e\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "4cd70fa4-1e6e-452a-8212-4be6138a6b34",
+    "pk": "55fe1f7d-f148-4465-9a37-5e4a52f466da",
     "fields": {
-        "element": "c16df32e-03b5-47e4-868f-98d57eac1f15",
-        "path": "[\"7a007e29-b509-4c4e-8321-78464df6f03a\"]",
+        "element": "9a43676e-46bd-4653-bdcd-73ded20c39ad",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"83d06bd7-3efc-4574-b833-62be2d1d94ad\"]",
         "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "4e407215-0ed2-4ff2-98ff-a22d6b23349a",
+    "pk": "5e98433f-20f0-4236-a9d0-c773a0713103",
     "fields": {
-        "element": "a8c108a2-a6b7-4401-9d64-65cf263531a7",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6c6a774e-88c3-40c5-9913-d7ec2a3d5125\"]",
-        "ordering": 1
+        "element": "ca255c31-ab61-4a34-b8a4-320f901a2cc9",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"be338e62-cfcd-4faa-9b23-9da3900313f9\"]",
+        "ordering": 3
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "56655c48-69b4-4ea0-a724-b10d1a47e231",
+    "pk": "653029f5-2c55-4b38-9f6c-8170e81cfb0d",
     "fields": {
-        "element": "b8c4d1e3-aa9d-425a-b70f-c8ff16ae147c",
-        "path": "[\"7a007e29-b509-4c4e-8321-78464df6f03a\"]",
-        "ordering": 2
+        "element": "2cdfa4f2-a66c-46c0-8038-8e5ac7e7d36e",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\"]",
+        "ordering": 3
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "5c064e36-d3b5-4a8b-8f2d-97da74e84a70",
+    "pk": "74874a72-894b-43b6-9626-c323c0467fe6",
     "fields": {
-        "element": "86f68e65-f23b-4d01-a9ca-c40eed9cda32",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 6
+        "element": "ef9367bc-5de0-4c3f-aebf-39086e11efaa",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"2adf861a-a36a-4127-bf21-583ff95bc08c\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "617f3ea6-17f8-4514-9f87-4c8bdd747e3f",
+    "pk": "7c2e8330-98c3-48ae-b974-f1974eaabacd",
     "fields": {
-        "element": "4d2f59fe-9808-44b1-b3b5-017490ae4064",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 2
+        "element": "be338e62-cfcd-4faa-9b23-9da3900313f9",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "6cc0a005-dbff-4b53-b452-b6d4d8ec8eb3",
+    "pk": "890f1f05-6c4e-417c-9459-2c603c1ba5aa",
     "fields": {
-        "element": "09ee64d6-3d8d-4d74-a1cd-ec34c8c2cd38",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"664f19d6-472d-49cc-bd87-26c19db7d2d3\"]",
-        "ordering": 0
+        "element": "7bf57bb3-1836-4b17-a3e0-a8981dae790b",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\"]",
+        "ordering": 7
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "8adb5d49-8651-4239-b98d-425a70ff39cf",
+    "pk": "89f2d847-69e2-4cdc-831d-ef595472c7f4",
     "fields": {
-        "element": "6a41ddf7-41e4-40ad-8234-d773b74058bc",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 1
+        "element": "e1355423-3976-43ca-842c-2e146ef7e79a",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"7bf57bb3-1836-4b17-a3e0-a8981dae790b\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a2d6b2bd-677e-4f72-9a9d-64957b854a90",
+    "pk": "93b0d14a-9a35-4bcc-b6be-2051a8951a10",
     "fields": {
-        "element": "18ada82e-36c6-47e4-bcff-bee5aa63889b",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"4d2f59fe-9808-44b1-b3b5-017490ae4064\"]",
-        "ordering": 2
+        "element": "b9320e75-eed0-431e-9902-f339b7e42702",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"3e73d3d0-a713-4d2f-a1e8-ec268a0e7df1\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a3d290d2-c6a1-4c80-8baf-37d42497e61e",
+    "pk": "a350fa3b-5988-4ab5-9294-168200e2136b",
     "fields": {
-        "element": "d227268a-2c2e-40ff-b22c-ab6e52eb5bba",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 3
+        "element": "3e73d3d0-a713-4d2f-a1e8-ec268a0e7df1",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\"]",
+        "ordering": 6
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a735c855-bd4b-4ece-8345-eac1dc791ff9",
+    "pk": "a8d8ad5f-4e11-4cd1-ba7c-d30070c02b82",
     "fields": {
-        "element": "ba888ed5-8c85-4f49-8f87-499219187ade",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
+        "element": "2adf861a-a36a-4127-bf21-583ff95bc08c",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\"]",
         "ordering": 5
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a8012a46-1e99-40a4-ad86-d2b2040eb026",
+    "pk": "aefdb812-83a8-4c75-abc3-ca4cbc6bdd9f",
     "fields": {
-        "element": "4c054d5e-f30c-4f70-a368-8e6f78f48ba1",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"d227268a-2c2e-40ff-b22c-ab6e52eb5bba\"]",
-        "ordering": 0
+        "element": "46ee25df-c8c1-46f8-af4c-d62d0c8f4600",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\"]",
+        "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "a8b4a820-4de5-4164-88e9-66b3fd875938",
+    "pk": "b07d77e6-c1a7-46c8-83ec-131852006367",
     "fields": {
-        "element": "34cdee1d-40e3-4499-b04b-93f6d2bfb802",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6c6a774e-88c3-40c5-9913-d7ec2a3d5125\"]",
+        "element": "2bd1a3ce-6073-4387-8a71-a5d0134ce894",
+        "path": "[\"67ca764b-de6a-41a8-885c-ce2ce422e391\"]",
         "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "ab00de7e-015c-4f76-a3c0-645cb879a024",
+    "pk": "b5b18993-cf3b-4204-800b-e7bc0b317fe6",
     "fields": {
-        "element": "2c0f28b1-b8e9-4f08-9014-75ce7950ee6f",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6a41ddf7-41e4-40ad-8234-d773b74058bc\"]",
-        "ordering": 1
+        "element": "83d06bd7-3efc-4574-b833-62be2d1d94ad",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\"]",
+        "ordering": 4
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "b100fd6e-d7ed-4481-8604-f07e9d554637",
+    "pk": "c2852eb9-9be8-49ce-9882-3dc1492a066a",
     "fields": {
-        "element": "3603393d-4831-4dbf-9672-3fbe1077e03a",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"4d2f59fe-9808-44b1-b3b5-017490ae4064\"]",
+        "element": "36345a26-69b9-4cf9-9682-5eb62d1726cc",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"46ee25df-c8c1-46f8-af4c-d62d0c8f4600\"]",
         "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "b7098cdc-dcba-4efc-8213-e92af55fa6f9",
+    "pk": "c9f3a671-5b6e-4930-8e0a-61e202de4b12",
     "fields": {
-        "element": "3f3bb3ee-ec7c-436c-9dde-f8aa0e1a69d7",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 7
+        "element": "b7e07d82-0d10-4060-843e-a838b766ec3d",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"51af1d15-e242-49e4-a797-7697c5162059\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "b992817b-343e-4a5b-bf6d-42cb334696d1",
+    "pk": "cf09a2a7-9630-437f-9183-4c1e1dc9909b",
     "fields": {
-        "element": "b0a0984b-dcf2-46da-8d6e-933020a46011",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6a41ddf7-41e4-40ad-8234-d773b74058bc\"]",
+        "element": "ae27310b-dc31-413b-b551-2b74c81147ab",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"46ee25df-c8c1-46f8-af4c-d62d0c8f4600\"]",
         "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "c35f1e16-f92d-4589-afc4-6c3115fa36d7",
+    "pk": "d1ae93db-ba88-4ab9-abbb-c61d2b6e87a3",
     "fields": {
-        "element": "9fd626f8-a5eb-4ae3-9f4f-f0ea4216712e",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"664f19d6-472d-49cc-bd87-26c19db7d2d3\"]",
-        "ordering": 1
+        "element": "bfea27aa-fb3a-4298-a71f-f131cd2e6a13",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"be338e62-cfcd-4faa-9b23-9da3900313f9\"]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "d0586c21-e60a-41d8-9a17-b71eeb75bc68",
+    "pk": "d4688c02-4cd6-4d53-b0df-89a4197c8b35",
     "fields": {
-        "element": "e2f8eee0-cf55-404c-86e6-6950ab88faac",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"3f3bb3ee-ec7c-436c-9dde-f8aa0e1a69d7\"]",
-        "ordering": 0
+        "element": "21250ac5-76b6-4c39-8ff2-60e77188501e",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"51af1d15-e242-49e4-a797-7697c5162059\"]",
+        "ordering": 2
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "da4fe1fb-7ad7-4b38-973f-c9a4ca8d402f",
+    "pk": "d86f1d72-da60-4696-b25c-d42c2338dee6",
     "fields": {
-        "element": "3189dc31-fa75-4c60-b1d0-7a15720e35ca",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"86f68e65-f23b-4d01-a9ca-c40eed9cda32\"]",
+        "element": "67ca764b-de6a-41a8-885c-ce2ce422e391",
+        "path": "[]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "e2c73d2f-8881-4b4e-909f-7244abd4c92c",
+    "pk": "da260b1c-ea75-41b0-8702-198c05b8b2ec",
     "fields": {
-        "element": "664f19d6-472d-49cc-bd87-26c19db7d2d3",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\"]",
-        "ordering": 4
+        "element": "5e14a51b-092b-450c-9239-56bc02ff880f",
+        "path": "[]",
+        "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "e6c19e34-49a7-4dc2-aa37-8d96f2a7b43e",
+    "pk": "f234f8df-58fe-46b1-8a1b-f4601babb42e",
     "fields": {
-        "element": "7a007e29-b509-4c4e-8321-78464df6f03a",
-        "path": "[]",
-        "ordering": 0
+        "element": "9523eff1-25a3-423f-8660-fa3b0ab51c93",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"be338e62-cfcd-4faa-9b23-9da3900313f9\"]",
+        "ordering": 1
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "e9d64de1-c0c6-44a1-bb33-38c87a144282",
+    "pk": "f35e8c1a-c09a-463e-bfe7-5aef6eea762e",
     "fields": {
-        "element": "d5daac1b-5f57-4d3c-b120-54adbcc82d7d",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"ba888ed5-8c85-4f49-8f87-499219187ade\"]",
+        "element": "e6c910b3-5dda-4db8-8ee0-2342e5f5d6ad",
+        "path": "[\"5e14a51b-092b-450c-9239-56bc02ff880f\", \"83d06bd7-3efc-4574-b833-62be2d1d94ad\"]",
         "ordering": 0
     }
 },
 {
     "model": "documents.elementpath",
-    "pk": "ed3283f8-0af1-467e-8a52-77f9d434af78",
+    "pk": "f415dff7-e248-4f3b-8b69-426b411a4dc6",
     "fields": {
-        "element": "92101c2b-885f-42e2-a0cb-13a8a04c0ea8",
-        "path": "[\"3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64\", \"6a41ddf7-41e4-40ad-8234-d773b74058bc\"]",
-        "ordering": 0
+        "element": "5d300cf4-3dae-43f1-9c91-26ed0c89064c",
+        "path": "[\"67ca764b-de6a-41a8-885c-ce2ce422e391\"]",
+        "ordering": 1
     }
 },
 {
     "model": "documents.element",
-    "pk": "09ee64d6-3d8d-4d74-a1cd-ec34c8c2cd38",
+    "pk": "21250ac5-76b6-4c39-8ff2-60e77188501e",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
+        "name": "DATUM",
         "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": "d484743f-e61a-4151-9c9c-cbc86031fe3e",
+        "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -828,18 +828,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "17a65610-023b-46c7-93a6-453335626d17",
+    "pk": "2adf861a-a36a-4127-bf21-583ff95bc08c",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "71cb6579-1b09-4bc9-9278-48f648fb6af6",
+        "name": "Act 3",
         "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": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -847,18 +847,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "18ada82e-36c6-47e4-bcff-bee5aa63889b",
+    "pk": "2bd1a3ce-6073-4387-8a71-a5d0134ce894",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "099df260-b6a6-4559-b091-f2f57a78640f",
+        "name": "Volume 2, page 2r",
         "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": "21636e84-7d76-4261-b09f-1dc3a16cf2db",
+        "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": "2cdfa4f2-a66c-46c0-8038-8e5ac7e7d36e",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "71cb6579-1b09-4bc9-9278-48f648fb6af6",
+        "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": "36345a26-69b9-4cf9-9682-5eb62d1726cc",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
+        "name": "ROY",
         "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": "56695e99-e1dc-4a94-b19f-061017b046e2",
+        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -904,18 +904,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "34cdee1d-40e3-4499-b04b-93f6d2bfb802",
+    "pk": "369a2f84-b6fc-48f4-8a8f-f5261674329c",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "eb4f8afb-4749-49a6-b3ba-d72be2d50384",
+        "name": "Surface A",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
-        "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
+        "image": "b7bc85ce-6999-4dd6-bb4e-5442de55c57e",
+        "polygon": "LINEARRING (0 0, 0 600, 600 600, 600 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -923,18 +923,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "3603393d-4831-4dbf-9672-3fbe1077e03a",
+    "pk": "3e73d3d0-a713-4d2f-a1e8-ec268a0e7df1",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "71cb6579-1b09-4bc9-9278-48f648fb6af6",
+        "name": "Act 4",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
-        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
+        "image": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -942,18 +942,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "3bcfeab1-e4d4-4f63-8f60-8fc5fc551a64",
+    "pk": "46ee25df-c8c1-46f8-af4c-d62d0c8f4600",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "099df260-b6a6-4559-b091-f2f57a78640f",
+        "name": "Volume 1, page 1v",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "56695e99-e1dc-4a94-b19f-061017b046e2",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -961,18 +961,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "3f3bb3ee-ec7c-436c-9dde-f8aa0e1a69d7",
+    "pk": "51af1d15-e242-49e4-a797-7697c5162059",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "099df260-b6a6-4559-b091-f2f57a78640f",
+        "name": "Volume 1, page 2r",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "d484743f-e61a-4151-9c9c-cbc86031fe3e",
+        "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": "5d300cf4-3dae-43f1-9c91-26ed0c89064c",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "099df260-b6a6-4559-b091-f2f57a78640f",
+        "name": "Volume 2, page 1v",
         "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": "888b07cf-6e17-4d1e-9b01-1186121b1cfb",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -999,18 +999,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "4c054d5e-f30c-4f70-a368-8e6f78f48ba1",
+    "pk": "5e14a51b-092b-450c-9239-56bc02ff880f",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "f9fa0ac3-c3a5-4916-b7a1-18de2de95918",
+        "name": "Volume 1",
         "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": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1018,18 +1018,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "4d2f59fe-9808-44b1-b3b5-017490ae4064",
+    "pk": "67ca764b-de6a-41a8-885c-ce2ce422e391",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "f9fa0ac3-c3a5-4916-b7a1-18de2de95918",
+        "name": "Volume 2",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
-        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
+        "image": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1037,18 +1037,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "60313262-3273-4c39-a8ee-cedc733f6990",
+    "pk": "7bf57bb3-1836-4b17-a3e0-a8981dae790b",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "71cb6579-1b09-4bc9-9278-48f648fb6af6",
+        "name": "Act 5",
         "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": null,
+        "polygon": null,
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1056,12 +1056,12 @@
 },
 {
     "model": "documents.element",
-    "pk": "664f19d6-472d-49cc-bd87-26c19db7d2d3",
+    "pk": "83d06bd7-3efc-4574-b833-62be2d1d94ad",
     "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",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "71cb6579-1b09-4bc9-9278-48f648fb6af6",
         "name": "Act 2",
         "creator": null,
         "worker_version": null,
@@ -1075,18 +1075,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "6a41ddf7-41e4-40ad-8234-d773b74058bc",
+    "pk": "9523eff1-25a3-423f-8660-fa3b0ab51c93",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
+        "name": "ROY",
         "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": "b7bc85ce-6999-4dd6-bb4e-5442de55c57e",
+        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1094,17 +1094,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "6c6a774e-88c3-40c5-9913-d7ec2a3d5125",
+    "pk": "9a43676e-46bd-4653-bdcd-73ded20c39ad",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "eb4f8afb-4749-49a6-b3ba-d72be2d50384",
+        "name": "Surface C",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
+        "image": "56695e99-e1dc-4a94-b19f-061017b046e2",
         "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -1113,18 +1113,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "7a007e29-b509-4c4e-8321-78464df6f03a",
+    "pk": "9b7b7b7a-0b5e-42ed-8cbc-75caf378d45e",
     "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 2",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
+        "name": "DATUM",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "b7bc85ce-6999-4dd6-bb4e-5442de55c57e",
+        "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1132,18 +1132,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "86f68e65-f23b-4d01-a9ca-c40eed9cda32",
+    "pk": "ae27310b-dc31-413b-b551-2b74c81147ab",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
+        "name": "DATUM",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "56695e99-e1dc-4a94-b19f-061017b046e2",
+        "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1151,17 +1151,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "92101c2b-885f-42e2-a0cb-13a8a04c0ea8",
+    "pk": "b7e07d82-0d10-4060-843e-a838b766ec3d",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
         "name": "PARIS",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "47f164d3-d26f-41e8-9df1-b538645d60b5",
+        "image": "d484743f-e61a-4151-9c9c-cbc86031fe3e",
         "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -1170,18 +1170,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "9fd626f8-a5eb-4ae3-9f4f-f0ea4216712e",
+    "pk": "b9320e75-eed0-431e-9902-f339b7e42702",
     "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 C",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "eb4f8afb-4749-49a6-b3ba-d72be2d50384",
+        "name": "Surface E",
         "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": "d484743f-e61a-4151-9c9c-cbc86031fe3e",
+        "polygon": "LINEARRING (300 300, 300 600, 600 600, 600 300, 300 300)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1189,18 +1189,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "a8c108a2-a6b7-4401-9d64-65cf263531a7",
+    "pk": "be338e62-cfcd-4faa-9b23-9da3900313f9",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "099df260-b6a6-4559-b091-f2f57a78640f",
+        "name": "Volume 1, page 1r",
         "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": "b7bc85ce-6999-4dd6-bb4e-5442de55c57e",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1208,18 +1208,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "b0a0984b-dcf2-46da-8d6e-933020a46011",
+    "pk": "bfea27aa-fb3a-4298-a71f-f131cd2e6a13",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
+        "name": "PARIS",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "47f164d3-d26f-41e8-9df1-b538645d60b5",
-        "polygon": "LINEARRING (700 700, 700 800, 800 800, 800 700, 700 700)",
+        "image": "b7bc85ce-6999-4dd6-bb4e-5442de55c57e",
+        "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1227,18 +1227,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "b8c4d1e3-aa9d-425a-b70f-c8ff16ae147c",
+    "pk": "ca255c31-ab61-4a34-b8a4-320f901a2cc9",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "3b0543d9-e822-42aa-90fc-c45af23524e1",
+        "name": "Text line",
         "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": "b7bc85ce-6999-4dd6-bb4e-5442de55c57e",
+        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1246,18 +1246,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "ba888ed5-8c85-4f49-8f87-499219187ade",
+    "pk": "d2fd3f5e-30b4-4f80-ad81-62d6cea02e2e",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
+        "name": "ROY",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "d484743f-e61a-4151-9c9c-cbc86031fe3e",
+        "polygon": "LINEARRING (400 400, 400 500, 500 500, 500 400, 400 400)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1265,18 +1265,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "c16df32e-03b5-47e4-868f-98d57eac1f15",
+    "pk": "e1355423-3976-43ca-842c-2e146ef7e79a",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "eb4f8afb-4749-49a6-b3ba-d72be2d50384",
+        "name": "Surface F",
         "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": "d484743f-e61a-4151-9c9c-cbc86031fe3e",
+        "polygon": "LINEARRING (600 600, 600 1000, 1000 1000, 1000 600, 600 600)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1284,18 +1284,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "d227268a-2c2e-40ff-b22c-ab6e52eb5bba",
+    "pk": "e6c910b3-5dda-4db8-8ee0-2342e5f5d6ad",
     "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",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "eb4f8afb-4749-49a6-b3ba-d72be2d50384",
+        "name": "Surface B",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": null,
-        "polygon": null,
+        "image": "b7bc85ce-6999-4dd6-bb4e-5442de55c57e",
+        "polygon": "LINEARRING (600 600, 600 1000, 1000 1000, 1000 600, 600 600)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1303,17 +1303,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "d3dc3f63-9894-434b-9715-7bd906b63e63",
+    "pk": "ef4e20a9-7991-4d62-a2e8-bf565c0c6b33",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "6e334f43-b407-4b4d-aa2a-553da2dddf65",
         "name": "PARIS",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
+        "image": "56695e99-e1dc-4a94-b19f-061017b046e2",
         "polygon": "LINEARRING (100 100, 100 200, 200 200, 200 100, 100 100)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -1322,17 +1322,17 @@
 },
 {
     "model": "documents.element",
-    "pk": "d5daac1b-5f57-4d3c-b120-54adbcc82d7d",
+    "pk": "ef9367bc-5de0-4c3f-aebf-39086e11efaa",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "eb4f8afb-4749-49a6-b3ba-d72be2d50384",
         "name": "Surface D",
         "creator": null,
         "worker_version": null,
         "worker_run": null,
-        "image": "d4db6162-7d7b-4839-a84f-756e74fef150",
+        "image": "d484743f-e61a-4151-9c9c-cbc86031fe3e",
         "polygon": "LINEARRING (0 0, 0 300, 300 300, 300 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
@@ -1341,18 +1341,18 @@
 },
 {
     "model": "documents.element",
-    "pk": "e2f8eee0-cf55-404c-86e6-6950ab88faac",
+    "pk": "fb316906-9851-4851-a3d6-2a82a2b0f066",
     "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": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "099df260-b6a6-4559-b091-f2f57a78640f",
+        "name": "Volume 2, page 1r",
         "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": "29e9787c-7af0-4964-8b76-e8f5de3e0675",
+        "polygon": "LINEARRING (0 0, 0 1000, 1000 1000, 1000 0, 0 0)",
         "rotation_angle": 0,
         "mirrored": false,
         "confidence": null
@@ -1360,79 +1360,79 @@
 },
 {
     "model": "documents.entitytype",
-    "pk": "4e1138f1-be4c-4205-834b-b2795e2bd1a3",
+    "pk": "141135ea-c4a4-42eb-ac1b-03f8fe7a6cb0",
     "fields": {
         "name": "date",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0"
     }
 },
 {
     "model": "documents.entitytype",
-    "pk": "564768cc-38be-45be-9e61-8dcade951c00",
+    "pk": "7bb9ed54-338e-4ca6-ba64-0801785ff671",
     "fields": {
-        "name": "location",
+        "name": "organization",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0"
     }
 },
 {
     "model": "documents.entitytype",
-    "pk": "a773549d-4adf-4dde-a595-3b2318f5cc0d",
+    "pk": "7d637285-d95b-4b7c-9368-cce209a69879",
     "fields": {
         "name": "person",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0"
     }
 },
 {
     "model": "documents.entitytype",
-    "pk": "e19eb34b-55eb-4a8f-8019-18eea8e62fba",
+    "pk": "a4aa202a-5087-4f86-a3d6-8742175ba9f2",
     "fields": {
-        "name": "number",
+        "name": "location",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0"
     }
 },
 {
     "model": "documents.entitytype",
-    "pk": "e96f8757-37d7-4457-9f2a-8e870cdadae5",
+    "pk": "ff3f7dc1-b950-4f9a-9cd8-27d8aa1b8c8e",
     "fields": {
-        "name": "organization",
+        "name": "number",
         "color": "ff0000",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a"
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0"
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "28e2570a-4022-4718-8827-5be712a53d4b",
+    "pk": "2906dc29-9d4e-4aab-bbbd-a62b0e015bbc",
     "fields": {
-        "element": "3603393d-4831-4dbf-9672-3fbe1077e03a",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "21250ac5-76b6-4c39-8ff2-60e77188501e",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "ROY",
+        "text": "DATUM",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "626eaeaf-ae24-4bc0-b1f6-78ad744a3645",
+    "pk": "3cf06526-6690-4c64-9b3e-6a8865e3e177",
     "fields": {
-        "element": "6c6a774e-88c3-40c5-9913-d7ec2a3d5125",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "b7e07d82-0d10-4060-843e-a838b766ec3d",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "Lorem ipsum dolor sit amet",
+        "text": "PARIS",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "62b7b9e6-0869-444c-aa8b-054fc124f6d9",
+    "pk": "46c00ebe-6d6e-47e3-ad0a-6b95561dca34",
     "fields": {
-        "element": "a8c108a2-a6b7-4401-9d64-65cf263531a7",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "d2fd3f5e-30b4-4f80-ad81-62d6cea02e2e",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
         "text": "ROY",
         "orientation": "horizontal-lr",
@@ -1441,123 +1441,123 @@
 },
 {
     "model": "documents.transcription",
-    "pk": "6f477447-1b65-44f6-a912-6ff6b24165a2",
+    "pk": "54079611-98bb-4791-9ffa-fe0d9f287a5a",
     "fields": {
-        "element": "2c0f28b1-b8e9-4f08-9014-75ce7950ee6f",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "bfea27aa-fb3a-4298-a71f-f131cd2e6a13",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "ROY",
+        "text": "PARIS",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "797bde96-8b76-4a9b-a4b9-b3378fcfdcdf",
+    "pk": "62426d09-a1e8-49d4-991e-4a0f71077abf",
     "fields": {
-        "element": "18ada82e-36c6-47e4-bcff-bee5aa63889b",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "ef4e20a9-7991-4d62-a2e8-bf565c0c6b33",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "DATUM",
+        "text": "PARIS",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "7b0f48b5-0580-47ef-9927-5e5fbdb749ff",
+    "pk": "7dadbfd7-356a-43da-9f08-c8e32439a8f2",
     "fields": {
-        "element": "469f4606-f5ea-406d-a0bd-6d48050c39ed",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "9b7b7b7a-0b5e-42ed-8cbc-75caf378d45e",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "PARIS",
+        "text": "DATUM",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "a94dabcd-9e37-4cbc-a45a-6e9a5a76b01a",
+    "pk": "8ce611e8-6064-4d74-935c-775d317ce6ce",
     "fields": {
-        "element": "d3dc3f63-9894-434b-9715-7bd906b63e63",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "be338e62-cfcd-4faa-9b23-9da3900313f9",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "PARIS",
+        "text": "Lorem ipsum dolor sit amet",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "e74d7029-86e6-41ad-beed-12f4b215fb57",
+    "pk": "98936cc4-a0c0-45ba-9a71-b83e721ef393",
     "fields": {
-        "element": "34cdee1d-40e3-4499-b04b-93f6d2bfb802",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "36345a26-69b9-4cf9-9682-5eb62d1726cc",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "DATUM",
+        "text": "ROY",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "eae51f9a-eb40-4d27-946f-274231062d30",
+    "pk": "b466e3de-f7db-4566-8110-16c14c60ae7d",
     "fields": {
-        "element": "92101c2b-885f-42e2-a0cb-13a8a04c0ea8",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "ae27310b-dc31-413b-b551-2b74c81147ab",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "PARIS",
+        "text": "DATUM",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.transcription",
-    "pk": "fb8d4786-a627-4049-9433-20755e7f3ca5",
+    "pk": "ee8fc9fb-f6fa-435a-b1fd-41aa8bbf54cf",
     "fields": {
-        "element": "b0a0984b-dcf2-46da-8d6e-933020a46011",
-        "worker_version": "70b6b804-b6f9-4efb-9d79-b8258c336eb7",
+        "element": "9523eff1-25a3-423f-8660-fa3b0ab51c93",
+        "worker_version": "f50622da-5cdc-406d-9c3d-2416da0100a0",
         "worker_run": null,
-        "text": "DATUM",
+        "text": "ROY",
         "orientation": "horizontal-lr",
         "confidence": 1.0
     }
 },
 {
     "model": "documents.allowedmetadata",
-    "pk": "7c15d98e-10f2-4c0b-809f-b65242d5e74b",
+    "pk": "0b3e7a6f-dbc5-4eea-8eda-8f4ed32d92cd",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "date",
-        "name": "date"
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "location",
+        "name": "location"
     }
 },
 {
     "model": "documents.allowedmetadata",
-    "pk": "819968c2-2433-4597-ac8b-852ed3397a9a",
+    "pk": "43f8eda2-3dc7-49ab-bf85-9ea2792e5d90",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "location",
-        "name": "location"
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "text",
+        "name": "folio"
     }
 },
 {
     "model": "documents.allowedmetadata",
-    "pk": "c079f5d7-f039-43a5-93c6-9019e8203e6b",
+    "pk": "f9047ccd-8459-4a0a-a919-e2cc146b79e8",
     "fields": {
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "type": "text",
-        "name": "folio"
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "type": "date",
+        "name": "date"
     }
 },
 {
     "model": "documents.metadata",
-    "pk": "03221610-3351-472d-9f7e-b23d7cbc7b92",
+    "pk": "2c4f50d5-e172-451d-b502-97e7ea7a78d6",
     "fields": {
-        "element": "6a41ddf7-41e4-40ad-8234-d773b74058bc",
+        "element": "2bd1a3ce-6073-4387-8a71-a5d0134ce894",
         "name": "folio",
         "type": "text",
-        "value": "1v",
+        "value": "2r",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1565,12 +1565,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "18459ddf-615f-47cc-8de6-11ab62446632",
+    "pk": "2d8b3817-7ba9-4d21-aad6-6eea3e2ccfd7",
     "fields": {
-        "element": "664f19d6-472d-49cc-bd87-26c19db7d2d3",
-        "name": "number",
+        "element": "be338e62-cfcd-4faa-9b23-9da3900313f9",
+        "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": "4ad9804a-9356-4dde-a9e4-aee88acfeab3",
     "fields": {
-        "element": "6c6a774e-88c3-40c5-9913-d7ec2a3d5125",
-        "name": "folio",
+        "element": "7bf57bb3-1836-4b17-a3e0-a8981dae790b",
+        "name": "number",
         "type": "text",
-        "value": "1r",
+        "value": "5",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1591,12 +1591,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "5712f834-778d-450b-ba46-86262c4a13f8",
+    "pk": "538685ab-a382-4488-aeee-e1892f8ef347",
     "fields": {
-        "element": "ba888ed5-8c85-4f49-8f87-499219187ade",
-        "name": "number",
+        "element": "46ee25df-c8c1-46f8-af4c-d62d0c8f4600",
+        "name": "folio",
         "type": "text",
-        "value": "3",
+        "value": "1v",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1604,9 +1604,9 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "7d8d0dc1-fcf6-4e47-9b03-25d5baaac590",
+    "pk": "6f9f136a-d2f9-4399-9ea6-b2f63e52c985",
     "fields": {
-        "element": "86f68e65-f23b-4d01-a9ca-c40eed9cda32",
+        "element": "3e73d3d0-a713-4d2f-a1e8-ec268a0e7df1",
         "name": "number",
         "type": "text",
         "value": "4",
@@ -1617,12 +1617,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "820d9d2f-c6d0-40df-9bce-cb9062f17b0b",
+    "pk": "8794103d-35cb-41c2-b963-9197adf8e187",
     "fields": {
-        "element": "60313262-3273-4c39-a8ee-cedc733f6990",
+        "element": "5d300cf4-3dae-43f1-9c91-26ed0c89064c",
         "name": "folio",
         "type": "text",
-        "value": "1r",
+        "value": "1v",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1630,12 +1630,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "93e2a919-9320-47ce-9b0f-de178dab40bc",
+    "pk": "9d9f745e-3e24-40bd-8d84-f3b21f5a21d0",
     "fields": {
-        "element": "3f3bb3ee-ec7c-436c-9dde-f8aa0e1a69d7",
+        "element": "2cdfa4f2-a66c-46c0-8038-8e5ac7e7d36e",
         "name": "number",
         "type": "text",
-        "value": "5",
+        "value": "1",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1643,12 +1643,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "9fc32596-bd62-4f8d-894d-abfeefc6157a",
+    "pk": "a7430848-c83e-4646-94c4-dfc026bb0ae0",
     "fields": {
-        "element": "4d2f59fe-9808-44b1-b3b5-017490ae4064",
-        "name": "folio",
+        "element": "83d06bd7-3efc-4574-b833-62be2d1d94ad",
+        "name": "number",
         "type": "text",
-        "value": "2r",
+        "value": "2",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1656,12 +1656,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "ba9916fa-52e6-4203-8db4-662f9083c70c",
+    "pk": "dedbe859-d13a-4d2c-86d1-3e48c44518be",
     "fields": {
-        "element": "c16df32e-03b5-47e4-868f-98d57eac1f15",
+        "element": "fb316906-9851-4851-a3d6-2a82a2b0f066",
         "name": "folio",
         "type": "text",
-        "value": "1v",
+        "value": "1r",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1669,12 +1669,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "bf7933a6-1646-40d0-be66-1306bc564481",
+    "pk": "e10c025b-c119-4857-8bd3-43393137b8a2",
     "fields": {
-        "element": "b8c4d1e3-aa9d-425a-b70f-c8ff16ae147c",
-        "name": "folio",
+        "element": "2adf861a-a36a-4127-bf21-583ff95bc08c",
+        "name": "number",
         "type": "text",
-        "value": "2r",
+        "value": "3",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1682,12 +1682,12 @@
 },
 {
     "model": "documents.metadata",
-    "pk": "cffe6d09-810c-4365-8107-af240d60097e",
+    "pk": "f4558644-4883-45fb-9b4c-7921ad287f64",
     "fields": {
-        "element": "d227268a-2c2e-40ff-b22c-ab6e52eb5bba",
-        "name": "number",
+        "element": "51af1d15-e242-49e4-a797-7697c5162059",
+        "name": "folio",
         "type": "text",
-        "value": "1",
+        "value": "2r",
         "entity": null,
         "worker_version": null,
         "worker_run": null
@@ -1710,12 +1710,12 @@
 },
 {
     "model": "images.image",
-    "pk": "2552f45d-6880-4a4c-b9c2-1091598a52b4",
+    "pk": "21636e84-7d76-4261-b09f-1dc3a16cf2db",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img4",
+        "path": "img6",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1724,12 +1724,12 @@
 },
 {
     "model": "images.image",
-    "pk": "47f164d3-d26f-41e8-9df1-b538645d60b5",
+    "pk": "29e9787c-7af0-4964-8b76-e8f5de3e0675",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img2",
+        "path": "img4",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1738,12 +1738,12 @@
 },
 {
     "model": "images.image",
-    "pk": "4b93eabe-b42d-4b88-8f31-a63d0258000a",
+    "pk": "56695e99-e1dc-4a94-b19f-061017b046e2",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
         "server": 1,
-        "path": "img6",
+        "path": "img2",
         "width": 1000,
         "height": 1000,
         "hash": null,
@@ -1752,7 +1752,7 @@
 },
 {
     "model": "images.image",
-    "pk": "5de01fda-de62-4d26-b8bf-5c3f4678de3b",
+    "pk": "888b07cf-6e17-4d1e-9b01-1186121b1cfb",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
@@ -1766,7 +1766,7 @@
 },
 {
     "model": "images.image",
-    "pk": "d3547a48-79f6-4c1f-8f16-c0be1a1da9fc",
+    "pk": "b7bc85ce-6999-4dd6-bb4e-5442de55c57e",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
@@ -1780,7 +1780,7 @@
 },
 {
     "model": "images.image",
-    "pk": "d4db6162-7d7b-4839-a84f-756e74fef150",
+    "pk": "d484743f-e61a-4151-9c9c-cbc86031fe3e",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
@@ -1794,56 +1794,56 @@
 },
 {
     "model": "users.right",
-    "pk": "066494da-9d28-44a8-ad04-0616cde80af0",
+    "pk": "27023448-93c5-4164-aed7-753f7c1d8764",
     "fields": {
-        "user": 2,
+        "user": 4,
         "group": null,
-        "content_type": 20,
-        "content_id": "fdbe4b5c-9475-4310-8877-eb07344e294a",
-        "level": 100
+        "content_type": 35,
+        "content_id": "bac5ff73-6f02-49bc-a063-00ff64494d39",
+        "level": 10
     }
 },
 {
     "model": "users.right",
-    "pk": "58ce1f20-32a1-4507-8edd-28fff7a1a77f",
+    "pk": "a2967ea5-e1de-462e-882e-9faaf600b53e",
     "fields": {
-        "user": 2,
+        "user": 3,
         "group": null,
-        "content_type": 12,
-        "content_id": "c19116b6-866f-4bb7-b447-3544629a8151",
-        "level": 10
+        "content_type": 35,
+        "content_id": "bac5ff73-6f02-49bc-a063-00ff64494d39",
+        "level": 50
     }
 },
 {
     "model": "users.right",
-    "pk": "9f94b10b-ffd6-4a87-885d-af494d41c850",
+    "pk": "a72c8509-4829-4790-a165-55d9cb3e96d9",
     "fields": {
-        "user": 3,
+        "user": 2,
         "group": null,
-        "content_type": 35,
-        "content_id": "c104ee5f-be60-4cdd-a738-10bf24601682",
-        "level": 50
+        "content_type": 20,
+        "content_id": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
+        "level": 100
     }
 },
 {
     "model": "users.right",
-    "pk": "b04e5fb5-dbca-4255-aae1-4c100c510c9d",
+    "pk": "aa443485-f321-4fcd-9bdf-6b8547e6982a",
     "fields": {
         "user": 2,
         "group": null,
         "content_type": 35,
-        "content_id": "c104ee5f-be60-4cdd-a738-10bf24601682",
+        "content_id": "bac5ff73-6f02-49bc-a063-00ff64494d39",
         "level": 100
     }
 },
 {
     "model": "users.right",
-    "pk": "da8297fb-5484-4511-8d29-0e8d6cbccc49",
+    "pk": "b4b624b8-128b-472c-99a3-79c7b86fd7cb",
     "fields": {
-        "user": 4,
+        "user": 2,
         "group": null,
-        "content_type": 35,
-        "content_id": "c104ee5f-be60-4cdd-a738-10bf24601682",
+        "content_type": 12,
+        "content_id": "ae9b2873-eca1-4e11-a3e6-9d825ea0882f",
         "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$yo6qgdx09Wc9jqWv5Ua46o$noUOYhVgP2jQOJKDegPh/hgA8K/IE4sSqGFGJZ11jeA=",
         "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$XjsB2WBbuUlUxXMFFHNP0S$HcqLCn8egh9bJc3NNa1YNsFKdqDQFrjZVbfce75Gd8s=",
         "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": "bac5ff73-6f02-49bc-a063-00ff64494d39",
     "fields": {
         "name": "User group",
         "public": false,
@@ -3943,15 +3943,15 @@
 },
 {
     "model": "ponos.farm",
-    "pk": "c19116b6-866f-4bb7-b447-3544629a8151",
+    "pk": "ae9b2873-eca1-4e11-a3e6-9d825ea0882f",
     "fields": {
         "name": "Wheat farm",
-        "seed": "03f762b3c505f0406a411b3fb3bbe4785c0e7763a05b515e7208b99ee93e955c"
+        "seed": "f05f8519260e328318528860a15595dd7f6401880018661243c57071c161c740"
     }
 },
 {
     "model": "ponos.task",
-    "pk": "4ca6c03f-9fee-4d44-8b6d-a3cfa5ae1ffb",
+    "pk": "e844ca04-2226-4425-8347-ddc8eb1c29ee",
     "fields": {
         "run": 0,
         "depth": 0,
@@ -3967,22 +3967,22 @@
         "agent": null,
         "requires_gpu": false,
         "gpu": null,
-        "process": "245bc206-350c-43d5-8db3-d98645c4eaa9",
+        "process": "111cabac-6f43-4ff3-b6a3-77f79eb7e7fb",
         "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": "AkuV/NInTwyu6jFjlOy1Hk38kQEZrUYJj7E6LhCfbz4=",
         "parents": []
     }
 },
 {
     "model": "ponos.artifact",
-    "pk": "9a1b1d5d-ec3d-4a21-89da-0e930292467d",
+    "pk": "00515030-0750-46ea-9aa5-62c8bcd0252f",
     "fields": {
-        "task": "4ca6c03f-9fee-4d44-8b6d-a3cfa5ae1ffb",
+        "task": "e844ca04-2226-4425-8347-ddc8eb1c29ee",
         "path": "/path/to/docker_build",
         "size": 42000,
         "content_type": "application/octet-stream",
@@ -3992,30 +3992,30 @@
 },
 {
     "model": "training.dataset",
-    "pk": "3c74b8de-7c42-4f2a-921f-a5ef957b80da",
+    "pk": "13da2dd3-d8cf-43c4-9f61-b2c7d4562d69",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
         "creator": 2,
         "task": null,
-        "name": "Second Dataset",
-        "description": "dataset number two",
+        "name": "First Dataset",
+        "description": "dataset number one",
         "state": "open",
         "sets": "[\"training\", \"test\", \"validation\"]"
     }
 },
 {
     "model": "training.dataset",
-    "pk": "87c1ca55-bb92-4b6d-90af-a2f50ae49df9",
+    "pk": "925b6d6a-1121-42d9-8946-ceada2921c66",
     "fields": {
         "created": "2020-02-02T01:23:45.678Z",
         "updated": "2020-02-02T01:23:45.678Z",
-        "corpus": "fdbe4b5c-9475-4310-8877-eb07344e294a",
+        "corpus": "a00441c1-d58f-4a66-9108-b9c0985d14d0",
         "creator": 2,
         "task": null,
-        "name": "First Dataset",
-        "description": "dataset number one",
+        "name": "Second Dataset",
+        "description": "dataset number two",
         "state": "open",
         "sets": "[\"training\", \"test\", \"validation\"]"
     }
diff --git a/arkindex/documents/tests/test_allowed_metadata.py b/arkindex/documents/tests/test_allowed_metadata.py
index b7c62412b114dfb23309b401164f3e66fddde8dc..00468e091ac75ee01643211caf7a8c01386fe888 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 d722c54e41a8cc713c466a6d7a1c6655640a2315..ce5ab5e7d739c82a33ee9bbbcff77d2f338af1c7 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 8075296695abe24a16f360f933c6e6a0a73516b9..3ef2499efb3e35ee188e6aa9f817a88f88560afe 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 2548632c65e6ba17971c84396867c9bd3355efb6..21360cb42866b9e4b77c88071e245743c98af012 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 6a49250e42c322687ac81016df36951c635238e5..fc6b747da335aabf97e8f0c20826808aa1a9c626 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 ece36ea4cdf64a9d5fceb2ca90052e76badcce42..8c84c55a09a11824ea657ed942224a4baf6aa356 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 e0ff7d2c025e72914275e173614aa1d4090290f8..efe439164b65fe9fe88eef608f5896e2d6998ec6 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 4270e08a162d2772075c2234102f9faf0257229f..04a2e5c434a2b6ededee676ff1d375ff08348b3f 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 4aeade12e3a58a9e11da5a6ffd2d85b1c9471624..884d60ccdb0399479e47bccad8277e7c6f9b1a10 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 2d913b9fca7aaee6b15e9c8f557020612bd657dc..8779b27b483573595ff5337c880afb740a10e584 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 006f9c31794e84be61197c4a8a600f8fd1a22102..b39fa8626123b79336754ccbcf48845159f6a5d4 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 64e562bb4f2ab604aa3b3d16286d99c8a113a7d4..f60e35e3e775ee56f1697b8cf07f28981987403f 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 f110bfeca987087394df3492eced1b0aa52f5e60..d29c2f7f98b72f6056ecad5af5e9724cc603bfbf 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 f9403f3745bb9a0e4da3b229db4d105876b27e22..da6312ac19d0fcd0863ce87752baa5bd7dcf95ef 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 1f590c4c14dc6f98d23786fa685758920d611cb7..fbea2d6d51a63b7004ae98f21c011e684424f4d0 100644
--- a/arkindex/documents/tests/test_destroy_worker_results.py
+++ b/arkindex/documents/tests/test_destroy_worker_results.py
@@ -42,9 +42,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(
@@ -65,7 +66,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)
 
@@ -83,7 +84,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}",
@@ -104,7 +105,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}"
@@ -125,7 +126,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",
@@ -146,7 +147,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}",
@@ -167,7 +168,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)})
                 + (
@@ -197,7 +198,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"
@@ -210,7 +211,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"
@@ -223,7 +224,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"
@@ -236,7 +237,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"
@@ -249,7 +250,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"
@@ -262,7 +263,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"
@@ -275,7 +276,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"
@@ -288,7 +289,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"
@@ -302,7 +303,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"
@@ -316,7 +317,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}"
@@ -334,7 +335,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"
@@ -367,7 +368,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"
@@ -390,7 +391,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}"
diff --git a/arkindex/documents/tests/test_edit_transcriptions.py b/arkindex/documents/tests/test_edit_transcriptions.py
index a3341ab3defaa75d5fda48ba0ede4ebf94ccbd5c..c756ea5ddc2100297dbcee1b32fffbea87c2d021 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 12667ba439ab7e386c258b12b8f28ba3180ca2bd..f71d1298e392a981604640bd7992651106c434a3 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 bed5b21d49ee7df46aa573dcacb33db113dd76ae..37d2882b3c70a74f5e32a8a567f426f538dc6e67 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 39c4dde2fd0c13e7fc9121688ed83babf5ee390f..28dcd2b9f6d6e064da369713368005dd7cbd2937 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 7bbd833e8f6777f78182ab98f3c0cbd94b11d712..7f883e4eea68a17881374e93b8caf03cbb056f27 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 8016de5ee6662a61351a4b93347d8b06ae5d51cf..d1ab1e81d1f199d7d83aa8bc842a8fff315264b1 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 a9a0ebe324e6741607d802c83d323bf99eee6224..6968b3cbbf588210a6503f4ed393617a9c5d8f81 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 9380575afc4e99fd5bed56ee94ad3e27c1d1b4a6..d667bd311f7b4f4ce5c6040a6c06e90f555f2340 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 956449c7d5b5b0a0d98b59c14a74a303840ddb5f..d524373efef234c5c5b7c9f82aa9f9d80698e86b 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 0268555882a0450a597fdaf4192be40131a655d3..01340642c3d0ad698423baa4dcd030c8f387607c 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 be3da68ab6847c77592ab4eccf572ed4779b06e2..4a0bd1c6db80460c8a9535cc91d320c3e8c3676c 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 1fac8693c47cdd6eb94b77e6769b017663560ebe..c4d42407cb0e2bac4410f7ece70b4aaadabe85a3 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 326d1134f21fd334c3e1fcc2a55967775527a91f..c8062471c5bc065bc50e7bf26612a6254e1e7fa9 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 53ac2d74994ffb02e291aacf23902485c072516b..2fe56cd2fe22fbf09f8c694c1d450e84f8da4ab7 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 d45eb8e8473d68dde488d78d4b04670486744a73..36970acf11db753ba11684eea97bf8862c913f72 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 cda2b19db5803181f687ecb611f1867c32064d5e..618f9aa9eee3ba2405e0e68be69c60212be4d232 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 86459ccc93b680e53f72eefe3f38fdf6359b9f68..2f1cb8a2a2eb9401b491e109155a64737354a117 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 474b809baa905d2b332c08a11d03ba235aeb15cf..714c26b87216e70bf020872c0a2ac64e1c71089c 100644
--- a/arkindex/ponos/admin.py
+++ b/arkindex/ponos/admin.py
@@ -5,7 +5,6 @@ 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
 
 
 class ArtifactInline(admin.TabularInline):
@@ -179,7 +178,6 @@ class FarmAdmin(admin.ModelAdmin):
     list_display = ("id", "name")
     fields = ("id", "name", "seed")
     readonly_fields = ("id",)
-    inlines = [UserMembershipInline, GroupMembershipInline]
 
 
 class ClearTextSecretForm(forms.ModelForm):
diff --git a/arkindex/ponos/tests/test_api.py b/arkindex/ponos/tests/test_api.py
index a416be308fb2226956345c1949b09d7e852096fd..e257c88dba1fe745f3f07dfbb11c70af6b9be14c 100644
--- a/arkindex/ponos/tests/test_api.py
+++ b/arkindex/ponos/tests/test_api.py
@@ -5,6 +5,7 @@ import uuid
 from io import BytesIO
 from pathlib import Path
 from textwrap import dedent
+from unittest import expectedFailure
 from unittest.mock import PropertyMock, call, patch, seal
 
 from botocore.exceptions import ClientError
@@ -164,6 +165,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 +197,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 +251,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)
 
@@ -1148,6 +1150,7 @@ class TestAPI(FixtureAPITestCase):
             )
             self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
 
+    @expectedFailure
     def test_update_task_requires_process_admin_corpus(self):
         self.process.creator = self.superuser
         self.process.save()
@@ -1168,6 +1171,7 @@ class TestAPI(FixtureAPITestCase):
                     )
                     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
@@ -1426,6 +1430,7 @@ class TestAPI(FixtureAPITestCase):
             )
             self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
 
+    @expectedFailure
     def test_partial_update_task_requires_process_admin_corpus(self):
         self.process.creator = self.superuser
         self.process.save()
@@ -1446,6 +1451,7 @@ class TestAPI(FixtureAPITestCase):
                     )
                     self.assertEqual(resp.status_code, status.HTTP_403_FORBIDDEN)
 
+    @expectedFailure
     def test_partial_update_task_requires_process_admin_repo(self):
         self.process.mode = ProcessMode.Repository
         self.process.corpus = None
@@ -2668,6 +2674,7 @@ class TestAPI(FixtureAPITestCase):
             },
         )
 
+    @expectedFailure
     def test_list_farms_guest(self):
         self.client.force_login(self.user)
         self.assertFalse(self.default_farm.is_available(self.user))
diff --git a/arkindex/ponos/tests/test_artifacts_api.py b/arkindex/ponos/tests/test_artifacts_api.py
index 76cf4e6659a0a373d6bc669aefbdcd5faeacb960..4ace04665fe0131d6107a70e224cc0ed96c6ba19 100644
--- a/arkindex/ponos/tests/test_artifacts_api.py
+++ b/arkindex/ponos/tests/test_artifacts_api.py
@@ -1,5 +1,6 @@
 import hashlib
 import uuid
+from unittest import expectedFailure
 
 from django.conf import settings
 from django.test import override_settings
@@ -87,6 +88,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 +110,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 +147,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)
 
@@ -244,7 +246,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}",
@@ -426,7 +428,7 @@ class TestAPI(FixtureAPITestCase):
                 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 +493,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 +513,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 +538,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"]),
                     )
@@ -590,7 +593,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/process/admin.py b/arkindex/process/admin.py
index f02159a5c134800de106993b78dffeedfdfc4de3..3349b5f2704fd950fa7735b47f637fc139eb93ef 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 26001aaab9e928cf9e0296b8b42d3d6fe4be5106..5c3978eaf0220610905137998bc007c65dd9df26 100644
--- a/arkindex/process/api.py
+++ b/arkindex/process/api.py
@@ -1061,7 +1061,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 (
@@ -1092,7 +1092,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.
 
@@ -1111,7 +1111,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 (
@@ -1130,7 +1130,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/tests/test_corpus_worker_runs.py b/arkindex/process/tests/test_corpus_worker_runs.py
index 92aebf153a842179e6650a2b72a7c0d94cd63f2e..2e62a89f37514e987717854ae844b7cc906307eb 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 79b0e98ba483fb972c6e4a4073b3122619284f73..327d2341edf4837092d62da2a4bd7bb1a2b4e4a0 100644
--- a/arkindex/process/tests/test_create_process.py
+++ b/arkindex/process/tests/test_create_process.py
@@ -180,7 +180,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
         """
@@ -197,7 +198,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"),
@@ -210,6 +215,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(
@@ -336,7 +344,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"),
                 {
@@ -375,7 +383,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"),
                 {
@@ -576,7 +584,7 @@ class TestCreateProcess(FixtureAPITestCase):
         self.assertFalse(self.corpus.worker_versions.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process_2.id)}),
                 {"worker_activity": True},
@@ -667,7 +675,7 @@ class TestCreateProcess(FixtureAPITestCase):
         )
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process_2.id)}),
                 {"use_cache": True},
@@ -715,7 +723,7 @@ class TestCreateProcess(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process_2.id)}),
                 {"use_gpu": True},
@@ -786,7 +794,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process.use_gpu = True
         process.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"use_gpu": "true"}
@@ -810,7 +818,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process.use_gpu = True
         process.save()
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"use_gpu": "true"}
@@ -829,7 +837,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process.datasets.add(self.corpus.datasets.first())
         process.versions.set([self.version_2, self.version_3])
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(10):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -842,7 +850,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(10):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -859,7 +867,7 @@ class TestCreateProcess(FixtureAPITestCase):
         process.datasets.add(self.corpus.datasets.first())
         process.versions.add(self.version_1)
 
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(10):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -875,7 +883,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(10):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
 
@@ -897,7 +905,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(15):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -934,7 +942,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,
@@ -966,7 +974,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"),
                 {
@@ -987,7 +995,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 454f3282c9682d4bf8589930f842be04896d95af..19608844a4ac051f095d97b32745b1cc984f3221 100644
--- a/arkindex/process/tests/test_create_s3_import.py
+++ b/arkindex/process/tests/test_create_s3_import.py
@@ -1,3 +1,5 @@
+from unittest.mock import call, patch
+
 from django.test import override_settings
 from django.urls import reverse
 from rest_framework import status
@@ -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(23), 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),
@@ -196,7 +203,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(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),
                 "bucket_name": "blah",
@@ -239,7 +246,8 @@ class TestCreateS3Import(FixtureTestCase):
             "INGEST_S3_SECRET_KEY": "its-secret-i-wont-tell-you",
         })
 
-    def test_farm_guest(self):
+    @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)
@@ -247,7 +255,7 @@ class TestCreateS3Import(FixtureTestCase):
 
         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",
@@ -260,7 +268,11 @@ 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)
+        self.assertEqual(get_max_level_mock.call_args, call(self.user, self.other_farm))
+
+    @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 +280,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 +291,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 2d673ad00c3c74a90b823e2a2494c9419301019b..97b722c5881e5342f29ee0c3bb1bcccd7ab0a22a 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 142708b1265e11c07eb29dd732d39d99f9fc928c..c8c4babe0cf21d796371309a0ca6a3cbe1ab2ea9 100644
--- a/arkindex/process/tests/test_process_datasets.py
+++ b/arkindex/process/tests/test_process_datasets.py
@@ -1,5 +1,5 @@
 import uuid
-from unittest.mock import patch
+from unittest.mock import call, patch
 
 from django.urls import reverse
 from rest_framework import status
@@ -65,17 +65,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"], [
@@ -125,21 +129,26 @@ 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(6):
+
+                with self.assertNumQueries(4):
                     response = self.client.post(
                         reverse("api:process-dataset", kwargs={"process": self.dataset_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."})
 
+                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:
@@ -148,7 +157,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}),
                     )
@@ -159,27 +168,17 @@ class TestProcessDatasets(FixtureAPITestCase):
     def test_create_process_mode_local(self):
         self.client.force_login(self.user)
         local_process = Process.objects.get(creator=self.user, mode=ProcessMode.Local)
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:process-dataset", kwargs={"process": local_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_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(10):
-            response = self.client.post(
-                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_create_wrong_process_uuid(self):
         self.client.force_login(self.test_user)
         wrong_id = uuid.uuid4()
-        with self.assertNumQueries(6):
+        with self.assertNumQueries(4):
             response = self.client.post(
                 reverse("api:process-dataset", kwargs={"process": wrong_id, "dataset": self.dataset2.id}),
             )
@@ -189,29 +188,35 @@ 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}),
             )
             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}),
             )
             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}),
             )
@@ -223,7 +228,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}),
             )
@@ -235,7 +240,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}),
             )
@@ -270,7 +275,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})
             )
@@ -280,29 +285,34 @@ 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(6):
+
+        with self.assertNumQueries(4):
             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)
         self.dataset_process.datasets.add(new_dataset)
         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}),
             )
@@ -317,7 +327,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}),
                     )
@@ -335,21 +345,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(9):
-            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}),
             )
@@ -359,7 +359,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}),
             )
diff --git a/arkindex/process/tests/test_process_elements.py b/arkindex/process/tests/test_process_elements.py
index 89d0e44de1564b3e4539719a7dfc55f4fc54ec46..e437584d2d8385b50909505b982fac772651a117 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 c60ac38b62feef8d5fda683bd1c0f205a99e0ad6..b0d2db1d1f13a4fdfaa35cf49ed5b80239efe44c 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
@@ -136,7 +137,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)
 
@@ -218,7 +219,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)
 
@@ -253,7 +254,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)
 
@@ -320,7 +321,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)
 
@@ -334,7 +335,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)
 
@@ -376,7 +377,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)
 
@@ -423,7 +424,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)
 
@@ -448,7 +449,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)
 
@@ -471,7 +472,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)
 
@@ -509,7 +510,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)
 
@@ -538,7 +539,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)
 
@@ -570,7 +571,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)
 
@@ -681,7 +682,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)
 
@@ -762,18 +763,10 @@ class TestProcesses(FixtureAPITestCase):
             {"__all__": ["Please wait activities to be initialized before deleting this process"]}
         )
 
-    def test_delete_repository_import_no_permission(self):
+    @expectedFailure
+    def test_delete_process_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):
-        """
-        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())
@@ -819,7 +812,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):
@@ -846,7 +839,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)
@@ -905,7 +898,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"},
@@ -922,7 +915,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"},
@@ -951,7 +944,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)},
@@ -962,6 +955,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
@@ -972,6 +966,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
@@ -997,7 +992,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"},
@@ -1017,7 +1012,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"},
@@ -1041,7 +1036,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},
@@ -1069,7 +1064,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"},
@@ -1158,7 +1153,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}),
                 {
@@ -1189,7 +1184,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}),
                         {
@@ -1224,7 +1219,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}),
                         {
@@ -1258,7 +1253,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}),
                 {
@@ -1286,9 +1281,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):
@@ -1317,7 +1312,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}),
                 {
@@ -1441,6 +1436,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)
@@ -1460,7 +1456,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}),
                 {
@@ -1529,9 +1525,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):
@@ -1555,7 +1551,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}),
                 {
@@ -1614,6 +1610,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
@@ -1642,7 +1639,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]})
@@ -1656,7 +1653,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)
 
@@ -1667,6 +1664,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)
@@ -1698,7 +1696,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)
 
@@ -1725,7 +1723,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)
 
@@ -1738,7 +1736,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)
 
@@ -1760,7 +1758,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)
 
@@ -1784,7 +1782,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)
@@ -1821,7 +1819,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)
@@ -1848,7 +1846,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)
@@ -1875,7 +1873,7 @@ class TestProcesses(FixtureAPITestCase):
 
         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)],
@@ -1929,7 +1927,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(38):
             response = self.client.post(reverse("api:files-process"), {
                 "files": [
                     str(self.pdf_df.id),
@@ -1962,7 +1960,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(26), 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",
@@ -2071,7 +2069,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(5):
             response = self.client.post(reverse("api:files-process"), {
                 "files": [str(self.iiif_df.id)],
                 "folder_type": "volume",
@@ -2117,7 +2115,7 @@ class TestProcesses(FixtureAPITestCase):
 
         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)],
@@ -2131,6 +2129,7 @@ class TestProcesses(FixtureAPITestCase):
         process = Process.objects.get(id=data["id"])
         self.assertEqual(process.farm, self.other_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)
@@ -2152,6 +2151,7 @@ class TestProcesses(FixtureAPITestCase):
             "farm_id": ["You do not have access to this farm."],
         })
 
+    @expectedFailure
     def test_from_files_default_farm_guest(self):
         self.client.force_login(self.user)
         self.assertEqual(self.version_with_model.worker_runs.count(), 0)
@@ -2250,7 +2250,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process2.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2265,7 +2265,7 @@ class TestProcesses(FixtureAPITestCase):
         )
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2288,7 +2288,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process2.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2306,7 +2306,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process2.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2324,7 +2324,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process2.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(15):
+        with self.assertNumQueries(10):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2373,7 +2373,7 @@ class TestProcesses(FixtureAPITestCase):
         )
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2397,7 +2397,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2421,7 +2421,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2438,7 +2438,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2454,7 +2454,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)}),
                 {
@@ -2479,7 +2479,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(18):
+        with self.assertNumQueries(13):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process2.id)})
             )
@@ -2511,7 +2511,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertFalse(process.tasks.exists())
 
         self.client.force_login(self.user)
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)})
             )
@@ -2532,7 +2532,7 @@ class TestProcesses(FixtureAPITestCase):
         workers_process.worker_runs.create(version=self.recognizer, parents=[], configuration=None)
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(20):
+        with self.assertNumQueries(15):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(workers_process.id)}),
                 {"farm": str(self.other_farm.id)}
@@ -2543,6 +2543,7 @@ class TestProcesses(FixtureAPITestCase):
         self.assertEqual(workers_process.state, State.Unscheduled)
         self.assertEqual(workers_process.farm_id, self.other_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)
@@ -2562,6 +2563,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)
@@ -2590,7 +2592,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)}
@@ -2609,7 +2611,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(6):
                 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)
@@ -2623,7 +2625,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(6):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"chunks": 43},
@@ -2695,7 +2697,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(5):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"use_cache": "true", "worker_activity": "true", "use_gpu": "true"}
@@ -2727,7 +2729,7 @@ class TestProcesses(FixtureAPITestCase):
 
         self.client.force_login(self.user)
 
-        with self.assertNumQueries(21):
+        with self.assertNumQueries(16):
             response = self.client.post(
                 reverse("api:process-start", kwargs={"pk": str(process.id)}),
                 {"use_cache": "true", "worker_activity": "true", "use_gpu": "true"}
@@ -2763,7 +2765,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(15):
             response = self.client.post(reverse("api:process-start", kwargs={"pk": str(process.id)}))
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
@@ -2865,7 +2867,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()
@@ -2938,7 +2940,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."]})
@@ -2960,11 +2962,12 @@ 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,
@@ -2982,7 +2985,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."})
@@ -3013,6 +3016,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()
@@ -3034,7 +3038,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(
@@ -3083,7 +3087,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)})
             )
@@ -3119,7 +3123,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 d5c32e7a163cfc05a0450efa57d33d5c5afc2444..0d1854cafb2f436491855def6e96e7cc1f132e1e 100644
--- a/arkindex/process/tests/test_repos.py
+++ b/arkindex/process/tests/test_repos.py
@@ -1,3 +1,4 @@
+
 from django.urls import reverse
 from rest_framework import status
 from rest_framework.serializers import DateTimeField
@@ -54,19 +55,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 +92,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 +123,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 +150,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 +167,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 +215,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_templates.py b/arkindex/process/tests/test_templates.py
index 753421c531df72d18077c8ebd6e195267c57ccc3..49f8b82e94667a81a5ffdf37533ef13bcfec8114 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 f42698d2de6d3d1c1cc89cbe7ee0bbf934faf58e..69c05a1d5bd1405141330764b141cef21d5a526a 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_worker_configurations.py b/arkindex/process/tests/test_worker_configurations.py
index e56799ee044655b397831ee3b8e6148ac0e23673..e1e7b9df25bc2a2aba5990b69019d370aad1f83a 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 8fbb6ea6e38f15a8ca0c468b73d091743c1f4b21..1116df9052a6886a7383621b2b94b3e7d8cc9af4 100644
--- a/arkindex/process/tests/test_workeractivity.py
+++ b/arkindex/process/tests/test_workeractivity.py
@@ -798,10 +798,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."})
@@ -895,7 +896,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 b7b9c51d1b4e40d8ce23140c5cff2e10bf4a8115..3e8e292a6e6d08e986a2107df5be98447a77f3d2 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 e66ecfc19f7e3ca401855a6f93e66fd684b70d20..4022f9865995fcafb50aaa4a86b4f48f7bcf7525 100644
--- a/arkindex/process/tests/test_workerruns.py
+++ b/arkindex/process/tests/test_workerruns.py
@@ -1,5 +1,6 @@
 import uuid
 from datetime import datetime, timezone
+from unittest.mock import call, patch
 
 from django.test import override_settings
 from django.urls import reverse
@@ -110,7 +111,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 +120,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 +176,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 +195,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 +213,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 +250,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 +274,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 +289,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 +306,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 +337,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 +376,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 +398,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 +460,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 +474,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 +487,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 +556,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 +596,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 +665,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 +727,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 +856,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)
 
@@ -1030,7 +1037,7 @@ class TestWorkerRuns(FixtureAPITestCase):
     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 +1071,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 +1090,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 +1143,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 +1163,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 +1199,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 +1262,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 +1324,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 +1393,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 +1411,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 +1446,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 +1483,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 +1498,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 +1530,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 +1545,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 +1573,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 +1600,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 +1621,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 +1646,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 +1678,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 +1726,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 +1819,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 +1911,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 +1992,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(
@@ -2018,14 +2053,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 +2069,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 +2121,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 +2143,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 +2205,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 +2266,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 +2332,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 +2350,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 +2385,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 +2421,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 +2435,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 +2461,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 +2475,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 +2503,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 +2530,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 +2550,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 +2575,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 +2607,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 +2654,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 +2745,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 +2834,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 +2915,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(
@@ -2909,7 +2972,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 +2999,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 +3042,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 +3063,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)})
             )
diff --git a/arkindex/process/tests/test_workers.py b/arkindex/process/tests/test_workers.py
index bae488074179f0a10a643a46a4ade0c264dfc145..98a2d31ffde4f0b32749db4cae37c35bda751646 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/project/api_v1.py b/arkindex/project/api_v1.py
index 616d44640bd7553d2075caa5b877cfbf0534848b..3677b5bb7d6c8887c04db27f45d702bc81095a18 100644
--- a/arkindex/project/api_v1.py
+++ b/arkindex/project/api_v1.py
@@ -136,19 +136,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 +320,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"),
diff --git a/arkindex/project/mixins.py b/arkindex/project/mixins.py
index 6276b450d8a1b34ac86c10b14f9dba2d0ded5d5f..b26f626dd65f02ab92b92cec71445b6c4cc109cb 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/tests/test_acl_mixin.py b/arkindex/project/tests/test_acl_mixin.py
index 60856b17d760be196f99b884fd374506398366dc..a36975c7b25dc51aed69b1f6049f8e7333a6e8f0 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/training/api.py b/arkindex/training/api.py
index 9e71b20fda5d40cc692055df8595ef8c1551ccf2..f0d7c7980b0e3fb39598b0c7637acd0f628afabf 100644
--- a/arkindex/training/api.py
+++ b/arkindex/training/api.py
@@ -333,7 +333,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")
@@ -384,7 +384,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 dde3c5abaf78821d9364c54e5cc3733515886d4a..b406a43d20667c08a6a1285deac62a727d166ef5 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
@@ -60,13 +60,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):
@@ -136,20 +141,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."},
@@ -160,7 +170,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."},
@@ -171,7 +181,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."},
@@ -182,7 +192,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={
@@ -196,7 +206,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={
@@ -210,7 +220,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"},
@@ -221,7 +231,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": ""},
@@ -232,7 +242,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": "             "},
@@ -243,7 +253,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."},
@@ -271,7 +281,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"},
@@ -296,7 +306,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"]},
@@ -320,7 +330,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": []},
@@ -331,7 +341,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"]},
@@ -342,7 +352,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"]},
@@ -359,7 +369,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={
@@ -408,9 +418,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={
@@ -423,9 +434,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={
@@ -440,7 +454,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={
@@ -455,7 +469,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={
@@ -470,7 +484,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"},
@@ -482,7 +496,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={
@@ -506,7 +520,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         """
         self.client.force_login(self.user)
         dataset_elt = self.dataset.dataset_elements.create(element=self.page1, set="training")
-        with self.assertNumQueries(11):
+        with self.assertNumQueries(8):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -538,7 +552,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(12):
+        with self.assertNumQueries(9):
             response = self.client.put(
                 reverse("api:dataset-update", kwargs={"pk": self.dataset.pk}),
                 data={
@@ -570,7 +584,7 @@ class TestDatasetsAPI(FixtureAPITestCase):
         No more than one set can be updated
         """
         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={
@@ -585,7 +599,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={
@@ -600,7 +614,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={
@@ -615,7 +629,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={
@@ -634,7 +648,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={
@@ -653,7 +667,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={
@@ -674,7 +688,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={
@@ -692,7 +706,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}",
@@ -757,7 +771,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={
@@ -776,7 +790,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}",
@@ -793,7 +807,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}",
@@ -833,9 +847,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"},
@@ -844,9 +859,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={
@@ -860,7 +878,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={
@@ -873,7 +891,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={
@@ -890,7 +908,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={
@@ -904,7 +922,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={
@@ -918,7 +936,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}",
@@ -933,7 +951,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}",
@@ -948,7 +966,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}",
@@ -964,7 +982,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={
@@ -1027,7 +1045,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={
@@ -1064,18 +1082,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"})
             )
@@ -1084,7 +1108,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})
             )
@@ -1107,7 +1131,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})
             )
@@ -1134,18 +1158,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"}),
             )
@@ -1154,7 +1182,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}),
             )
@@ -1169,7 +1197,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}),
                     )
@@ -1187,7 +1215,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}),
             )
@@ -1211,20 +1239,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"}
@@ -1241,7 +1273,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"},
@@ -1449,7 +1481,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},
@@ -1474,18 +1506,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})
             )
@@ -1494,9 +1531,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(), {
@@ -1507,7 +1547,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)},
@@ -1520,7 +1560,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)},
@@ -1535,7 +1575,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)},
@@ -1547,7 +1587,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)},
@@ -1558,7 +1598,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)},
@@ -1594,18 +1634,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})
             )
@@ -1614,9 +1659,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(), {
@@ -1626,7 +1674,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"},
@@ -1640,7 +1688,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},
@@ -1656,7 +1704,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},
@@ -1669,7 +1717,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},
@@ -1689,7 +1737,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},
@@ -1705,13 +1753,17 @@ class TestDatasetsAPI(FixtureAPITestCase):
             ]
         )
 
-    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")
@@ -1757,7 +1809,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(), {
@@ -1839,26 +1891,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
@@ -1870,7 +1932,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",
@@ -1912,7 +1974,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",
@@ -1946,7 +2008,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",
@@ -1979,13 +2041,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)})
@@ -1997,6 +2060,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):
@@ -2091,7 +2157,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 4876b9904bfe6e5e6647b9336598948e6f3c1557..a1efc6de3c6b6ceeb0f515a73d59536f4f73ad53 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 b69071b15881665c0ef02af8e5e8bab00a7be58f..7a264b2f3f26fa529d8000960acfe5e8b73f84ee 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 f163dc0e0cc4fccc498ffa1c515a2c00bdab9fed..20a19ff5b7ea233d6b306dc088aaa8ec97a3c4b9 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 3d9d1a9129b7b809772b12aaec6ed14c4605fd4f..a50a388cf896b6d984f81d4871f161c8a73252c7 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 0000000000000000000000000000000000000000..2704635a321093439715980147af74697ffb9bdd
--- /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 656111cd18123ce7a18a64dcb6afcf4eb628e380..443c756abfc761a9176c781b996b6afac5a8dd8c 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,37 +15,22 @@ 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__)
 
@@ -290,23 +271,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):
     """
@@ -353,287 +317,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 42fb755c08f3c5362302457d54297763a913ca71..705ef7d16f9a211fd79a7054c2254431922a9a34 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 9371345afe2b98f829fce64c3169cb2c45b8d97b..0000000000000000000000000000000000000000
--- 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 fe7665abb5f2cd0f04083079f37e6b51df883c66..0000000000000000000000000000000000000000
--- 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 f0f9e6206e17a792c565bf63ee2cf2975ac77d3c..7376532c48c63f881f7eb40cfc7a5818b10c54bc 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 201708f558ad103de15a08db58d1485f181c101f..9fcf143b03b683259c653bf224b8b3f68c35af23 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")