From ef227128120de7bcec3c68a00238469c7c4c5c8b Mon Sep 17 00:00:00 2001
From: Erwan Rouchet <rouchet@teklia.com>
Date: Tue, 28 May 2024 13:49:44 +0000
Subject: [PATCH] Upgrade spectacular to the latest version

---
 arkindex/documents/api/elements.py       |  7 +++----
 arkindex/documents/api/export.py         |  4 ++--
 arkindex/ponos/api.py                    |  4 ++--
 arkindex/process/api.py                  |  4 +++-
 arkindex/process/tests/test_processes.py | 11 +++++++----
 arkindex/project/openapi/schema.py       |  3 ++-
 arkindex/project/settings.py             |  3 +++
 arkindex/training/api.py                 |  4 +---
 arkindex/training/serializers.py         |  4 ++--
 arkindex/users/api.py                    |  4 ++--
 arkindex/users/serializers.py            |  8 +++-----
 requirements.txt                         |  2 +-
 12 files changed, 31 insertions(+), 27 deletions(-)

diff --git a/arkindex/documents/api/elements.py b/arkindex/documents/api/elements.py
index f08ae65497..2116376656 100644
--- a/arkindex/documents/api/elements.py
+++ b/arkindex/documents/api/elements.py
@@ -573,14 +573,12 @@ class ElementsListBase(CorpusACLMixin, DestroyModelMixin, ListAPIView):
 
     @cached_property
     def selected_corpus(self):
-        # Retrieve the corpus and check user access rights only once
+        # Retrieve the corpus only once
         corpus_id = self.kwargs.get("corpus")
         if corpus_id is None:
             return
 
-        corpus = get_object_or_404(Corpus, id=corpus_id)
-        self.check_corpus_access(corpus)
-        return corpus
+        return get_object_or_404(Corpus, id=corpus_id)
 
     @property
     def folder_filter(self):
@@ -1041,6 +1039,7 @@ class CorpusElements(ElementsListBase):
     def get_queryset(self):
         # Should not be possible due to the URL
         assert self.selected_corpus, "Missing corpus ID"
+        self.check_corpus_access(self.selected_corpus)
         return self.selected_corpus.elements.all()
 
     def get_filters(self) -> Q:
diff --git a/arkindex/documents/api/export.py b/arkindex/documents/api/export.py
index c2f6a52f96..9cec9a8351 100644
--- a/arkindex/documents/api/export.py
+++ b/arkindex/documents/api/export.py
@@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404
 from django.utils import timezone
 from django.utils.functional import cached_property
 from drf_spectacular.utils import extend_schema, extend_schema_view
-from rest_framework import permissions, serializers, status
+from rest_framework import permissions, status
 from rest_framework.exceptions import PermissionDenied, ValidationError
 from rest_framework.generics import ListCreateAPIView, RetrieveDestroyAPIView
 from rest_framework.response import Response
@@ -78,7 +78,7 @@ class CorpusExportAPIView(ListCreateAPIView):
             Guest access is required on private corpora.
             """
         ),
-        responses={302: serializers.Serializer},
+        responses={302: None},
     ),
     delete=extend_schema(
         operation_id="DestroyExport",
diff --git a/arkindex/ponos/api.py b/arkindex/ponos/api.py
index 368a44055b..a0a43c5ed8 100644
--- a/arkindex/ponos/api.py
+++ b/arkindex/ponos/api.py
@@ -4,7 +4,7 @@ from textwrap import dedent
 from django.db import transaction
 from django.shortcuts import get_object_or_404, redirect
 from drf_spectacular.utils import extend_schema, extend_schema_view
-from rest_framework import serializers, status
+from rest_framework import status
 from rest_framework.authentication import SessionAuthentication, TokenAuthentication
 from rest_framework.exceptions import NotFound, ValidationError
 from rest_framework.generics import CreateAPIView, ListCreateAPIView, RetrieveUpdateAPIView, UpdateAPIView
@@ -200,12 +200,12 @@ class TaskUpdate(UpdateAPIView):
             The task must be in a final state to be restarted.
             """
         ),
+        request=None,
         responses={201: TaskSerializer},
     ),
 )
 class TaskRestart(ProcessACLMixin, CreateAPIView):
     permission_classes = (IsVerified,)
-    serializer_class = serializers.Serializer
 
     def get_task(self):
         task = get_object_or_404(
diff --git a/arkindex/process/api.py b/arkindex/process/api.py
index 154f1273ba..b291def105 100644
--- a/arkindex/process/api.py
+++ b/arkindex/process/api.py
@@ -1327,7 +1327,7 @@ class WorkerRunList(ProcessACLMixin, ListCreateAPIView):
             201: WorkerRunSerializer,
             400: OpenApiResponse(
                 response=inline_serializer(
-                    name="Error message with ID",
+                    name="ErrorWithID",
                     fields={
                         "detail": serializers.ListField(child=serializers.CharField(), required=False),
                         "id": serializers.UUIDField(required=False, help_text="ID of an existing worker run")
@@ -1557,6 +1557,7 @@ class WorkerRunDetails(ProcessACLMixin, RetrieveUpdateDestroyAPIView):
             False: ProcessElementLightSerializer,
         },
         resource_type_field_name="with_image",
+        many=True,
     ),
     tags=["process"]
 ))
@@ -2193,6 +2194,7 @@ class S3ImportCreate(CreateAPIView):
     post=extend_schema(
         operation_id="SelectProcessFailures",
         tags=["process"],
+        request=None,
         responses={204: None},
     ),
 )
diff --git a/arkindex/process/tests/test_processes.py b/arkindex/process/tests/test_processes.py
index 7140e8a3ef..d0466ec05a 100644
--- a/arkindex/process/tests/test_processes.py
+++ b/arkindex/process/tests/test_processes.py
@@ -3049,18 +3049,21 @@ class TestProcesses(FixtureAPITestCase):
         self.elts_process.activity_state = ActivityState.Ready
         self.elts_process.save()
         self.client.force_login(self.user)
+
         for state in unfinished_states:
             with self.subTest(state=state):
                 self.elts_process.tasks.update(state=state)
                 self.assertEqual(self.elts_process.state, state)
+
                 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(
-                response.json(),
-                {"__all__": ["The process must be finished to select elements with failures."]},
-            )
+
+                self.assertDictEqual(
+                    response.json(),
+                    {"__all__": ["The process must be finished to select elements with failures."]},
+                )
 
     def test_select_failed_elts_no_failure(self):
         self.elts_process.run()
diff --git a/arkindex/project/openapi/schema.py b/arkindex/project/openapi/schema.py
index b773799137..4cac2e090c 100644
--- a/arkindex/project/openapi/schema.py
+++ b/arkindex/project/openapi/schema.py
@@ -32,5 +32,6 @@ class AutoSchema(BaseAutoSchema):
         Spectacular does not include any pagination parameters.
         """
         operation = super().get_operation(*args, **kwargs)
-        operation["x-paginated"] = self._is_list_view() and self._get_paginator() is not None
+        if operation is not None:
+            operation["x-paginated"] = self._is_list_view() and self._get_paginator() is not None
         return operation
diff --git a/arkindex/project/settings.py b/arkindex/project/settings.py
index 30feb9bf98..fe463ed963 100644
--- a/arkindex/project/settings.py
+++ b/arkindex/project/settings.py
@@ -219,6 +219,9 @@ SIMPLE_JWT = {
 
 SPECTACULAR_SETTINGS = {
     "CAMELIZE_NAMES": True,
+    # Remove the automatically generated `description` that lists the members of all enums,
+    # which duplicates the enum choices already displayed by ReDoc and does not add any documentation
+    "ENUM_GENERATE_CHOICE_DESCRIPTION": False,
     "SCHEMA_PATH_PREFIX": "/api/v1/",
     "TITLE": "Arkindex API",
     "CONTACT": {
diff --git a/arkindex/training/api.py b/arkindex/training/api.py
index 77289fab01..20f10a203f 100644
--- a/arkindex/training/api.py
+++ b/arkindex/training/api.py
@@ -522,9 +522,7 @@ class ModelCompatibleWorkerManage(CreateAPIView, DestroyAPIView):
             "Retrieve a download url for a model_version. A secret token is required to have access to the model version."
             "This endpoint does **not** require to be logged in."
         ),
-        responses={
-            302: serializers.Serializer,
-        },
+        responses={302: None},
         parameters=[
             OpenApiParameter(
                 "token",
diff --git a/arkindex/training/serializers.py b/arkindex/training/serializers.py
index 2f658f8b73..48ec505718 100644
--- a/arkindex/training/serializers.py
+++ b/arkindex/training/serializers.py
@@ -171,7 +171,7 @@ class ModelVersionLightSerializer(serializers.ModelSerializer):
 
     model = ModelLightSerializer(default=_model_from_context)
     state = EnumField(ModelVersionState)
-    configuration = serializers.JSONField(style={"base_template": "textarea.html"})
+    configuration = serializers.DictField(style={"base_template": "textarea.html"})
 
     class Meta:
         model = ModelVersion
@@ -190,7 +190,7 @@ class ModelVersionSerializer(serializers.ModelSerializer):
         allow_null=True,
     )
     description = serializers.CharField(required=False, style={"base_template": "textarea.html"})
-    configuration = serializers.JSONField(required=False, decoder=None, encoder=None, style={"base_template": "textarea.html"})
+    configuration = serializers.DictField(required=False, style={"base_template": "textarea.html"})
     tag = serializers.CharField(allow_null=True, max_length=50, required=False, default=None)
     state = EnumField(ModelVersionState, read_only=True)
     s3_url = serializers.SerializerMethodField(read_only=True, help_text="Only returned if the user has **contributor** access to the model, and the model is available.")
diff --git a/arkindex/users/api.py b/arkindex/users/api.py
index 5bc55e012c..e4bfc44db7 100644
--- a/arkindex/users/api.py
+++ b/arkindex/users/api.py
@@ -13,7 +13,7 @@ from django_rq.queues import get_queue
 from django_rq.settings import QUEUES
 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 import status
 from rest_framework.exceptions import AuthenticationFailed, NotFound, PermissionDenied, ValidationError
 from rest_framework.generics import CreateAPIView, ListAPIView, RetrieveDestroyAPIView, RetrieveUpdateDestroyAPIView
 from rest_framework.response import Response
@@ -180,7 +180,7 @@ class UserEmailVerification(APIView):
                 required=True,
             )
         ],
-        responses={302: serializers.Serializer}
+        responses={302: None},
     )
     def get(self, *args, **kwargs):
         if not all(arg in self.request.GET for arg in ("email", "token")):
diff --git a/arkindex/users/serializers.py b/arkindex/users/serializers.py
index 8bf49eeea6..d30ba1c369 100644
--- a/arkindex/users/serializers.py
+++ b/arkindex/users/serializers.py
@@ -161,9 +161,7 @@ class JobSerializer(serializers.Serializer):
     ended_at = serializers.DateTimeField(read_only=True, allow_null=True)
 
     def get_status(self, instance) -> str:
-        """
-        Avoid causing more Redis queries to fetch a job's current status
-        Note that a job status is part of a JobStatus enum,
-        but the enum is just a plain object and not an Enum for Py2 compatibility.
-        """
+        # Avoid causing more Redis queries to fetch a job's current status
+        # Note that a job status is part of a JobStatus enum,
+        # but the enum is just a plain object and not an Enum for Py2 compatibility.
         return instance.get_status(refresh=False)
diff --git a/requirements.txt b/requirements.txt
index dc936a8639..0c6424ce3e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,7 @@ django-rq==2.10.1
 djangorestframework==3.13.1
 djangorestframework-simplejwt==5.2.2
 docker==7.0.0
-drf-spectacular==0.21.2
+drf-spectacular==0.27.2
 python-magic==0.4.27
 python-memcached==1.59
 pytz==2023.3
-- 
GitLab