From 95c9482910d05252df93650e10cdd53c234c35b4 Mon Sep 17 00:00:00 2001
From: Valentin Rigal <rigal@teklia.com>
Date: Tue, 27 Aug 2024 08:27:33 +0000
Subject: [PATCH] Allow updating element image or polygon to null

---
 arkindex/documents/serializers/elements.py    | 57 +++++++++----------
 .../documents/tests/test_patch_elements.py    | 35 ++++++++++++
 arkindex/documents/tests/test_put_elements.py | 19 +++++++
 3 files changed, 80 insertions(+), 31 deletions(-)

diff --git a/arkindex/documents/serializers/elements.py b/arkindex/documents/serializers/elements.py
index 1d599b5da7..7e08129b4b 100644
--- a/arkindex/documents/serializers/elements.py
+++ b/arkindex/documents/serializers/elements.py
@@ -549,6 +549,7 @@ class ElementSerializer(ElementTinySerializer):
         queryset=Image.objects.all().select_related("server"),
         required=False,
         write_only=True,
+        allow_null=True,
         help_text="Link this element to an image by UUID via a polygon. "
                   "When the image is updated, if there was an image before and the polygon is not updated, "
                   "the previous polygon is reused. Otherwise, a polygon filling the new image is used.",
@@ -557,6 +558,7 @@ class ElementSerializer(ElementTinySerializer):
     polygon = LinearRingField(
         required=False,
         write_only=True,
+        allow_null=True,
         help_text="Set the polygon linking this element to the image. "
                   "`image` must be set when this field is set and there was no image or polygon defined before.",
     )
@@ -627,39 +629,32 @@ class ElementSerializer(ElementTinySerializer):
             return element.thumbnail.s3_put_url
 
     def update(self, instance, validated_data):
-        image = validated_data.pop("image", None)
-        polygon = validated_data.pop("polygon", None)
-        if polygon or image:
-            if not image:
-                if not instance.image_id:
-                    # A polygon was set but there is no image
-                    raise ValidationError({
-                        "image": ["Image is required when defining a polygon on an element without an existing zone"],
-                    })
-                image = instance.image
-
-            if image.width == 0 or image.height == 0:
-                raise ValidationError({"image": ["This image does not have valid dimensions."]})
-
-            if not polygon:
-                if instance.polygon:
-                    polygon = instance.polygon
-                else:
-                    polygon = LinearRing(
-                        (0, 0),
-                        (0, image.height),
-                        (image.width, image.height),
-                        (image.width, 0),
-                        (0, 0)
-                    )
-        if polygon and image:
-            if polygon_outside_image(image, polygon):
-                raise ValidationError({
-                    "polygon": ["An element's polygon must not exceed its image's bounds."]
-                })
+        image = validated_data.pop("image", instance.image)
+        polygon = validated_data.pop("polygon", instance.polygon)
+        if polygon and not image:
+            raise ValidationError({
+                "image": ["Image is required when defining a polygon on an element without an existing zone"],
+            })
+
+        if image and (image.width == 0 or image.height == 0):
+            raise ValidationError({"image": ["This image does not have valid dimensions."]})
+
+        if image and polygon and polygon_outside_image(image, polygon):
+            raise ValidationError({
+                "polygon": ["An element's polygon must not exceed its image's bounds."]
+            })
 
-            validated_data.update(image=image, polygon=polygon)
+        if image and not polygon:
+            # Automatically set a polygon corresponding to the full image
+            polygon = LinearRing(
+                (0, 0),
+                (0, image.height),
+                (image.width, image.height),
+                (image.width, 0),
+                (0, 0)
+            )
 
+        validated_data.update(image=image, polygon=polygon)
         return super().update(instance, validated_data)
 
 
diff --git a/arkindex/documents/tests/test_patch_elements.py b/arkindex/documents/tests/test_patch_elements.py
index ce5d1f008b..18354726d1 100644
--- a/arkindex/documents/tests/test_patch_elements.py
+++ b/arkindex/documents/tests/test_patch_elements.py
@@ -267,3 +267,38 @@ class TestPatchElements(FixtureAPITestCase):
         self.assertDictEqual(response.json(), {
             "polygon": ["An element's polygon must not exceed its image's bounds."]
         })
+
+    def test_patch_element_null_polygon_null_image(self):
+        self.assertIsNotNone(self.element.image)
+        self.assertIsNotNone(self.element.polygon)
+        self.client.force_login(self.user)
+        response = self.client.patch(
+            reverse("api:element-retrieve", kwargs={"pk": str(self.element.id)}),
+            data={
+                "polygon": None,
+                "image": None,
+            },
+            format="json",
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.element.refresh_from_db()
+        self.assertIsNone(self.element.image)
+        self.assertIsNone(self.element.polygon)
+
+    def test_patch_element_image_only(self):
+        self.element.image = None
+        self.element.polygon = None
+        self.element.save()
+        self.client.force_login(self.user)
+        response = self.client.patch(
+            reverse("api:element-retrieve", kwargs={"pk": str(self.element.id)}),
+            data={"image": str(self.image.id)},
+            format="json",
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.element.refresh_from_db()
+        self.assertEqual(self.element.image_id, self.image.id)
+        self.assertTupleEqual(
+            self.element.polygon.coords,
+            ((0, 0), (0, 42), (42, 42), (42, 0), (0, 0)),
+        )
diff --git a/arkindex/documents/tests/test_put_elements.py b/arkindex/documents/tests/test_put_elements.py
index 22cb9fce4c..babf90c779 100644
--- a/arkindex/documents/tests/test_put_elements.py
+++ b/arkindex/documents/tests/test_put_elements.py
@@ -278,3 +278,22 @@ class TestPutElements(FixtureAPITestCase):
         self.assertDictEqual(response.json(), {
             "polygon": ["An element's polygon must not exceed its image's bounds."]
         })
+
+    def test_put_element_null_polygon_null_image(self):
+        self.assertIsNotNone(self.element.image)
+        self.assertIsNotNone(self.element.polygon)
+        self.client.force_login(self.user)
+        response = self.client.put(
+            reverse("api:element-retrieve", kwargs={"pk": str(self.element.id)}),
+            data={
+                "name": self.element.name,
+                "type": self.element.type.slug,
+                "polygon": None,
+                "image": None,
+            },
+            format="json",
+        )
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.element.refresh_from_db()
+        self.assertIsNone(self.element.polygon)
+        self.assertIsNone(self.element.image)
-- 
GitLab