From 84471ea39cbfd8a9bb86308815fc1249595bef43 Mon Sep 17 00:00:00 2001 From: gpetrak Date: Thu, 3 Apr 2025 18:02:07 +0300 Subject: [PATCH 1/4] adding delete_thumbnail endpoint --- geonode/base/api/views.py | 56 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index a7ad7df6ddb..9749d5aee48 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -57,7 +57,7 @@ from geonode.base.models import Configuration, ExtraMetadata, LinkedResource from geonode.thumbs.exceptions import ThumbnailError from geonode.thumbs.thumbnails import create_thumbnail -from geonode.thumbs.utils import _decode_base64, BASE64_PATTERN +from geonode.thumbs.utils import _decode_base64, BASE64_PATTERN, remove_thumb from geonode.groups.conf import settings as groups_settings from geonode.base.models import HierarchicalKeyword, Region, ResourceBase, TopicCategory, ThesaurusKeyword from geonode.base.api.filters import ( @@ -721,6 +721,60 @@ def set_thumbnail_from_bbox(self, request, resource_id, *args, **kwargs): logger.error(e) return Response(data={"message": e.args[0], "success": False}, status=500, exception=True) + @extend_schema( + methods=["post"], + responses={200}, + description="API endpoint allowing to delete a thumbnail for an existing dataset.", + ) + @action( + detail=False, + url_path="(?P\d+)/delete_thumbnail", # noqa + url_name="delete-thumbnail", + methods=["post"], + permission_classes=[IsAuthenticated, UserHasPerms(perms_dict={"default": {"POST": ["base.add_resourcebase"]}})], + ) + + def delete_thumbnail(self, request, resource_id, *args, **kwargs): + + try: + resource = ResourceBase.objects.get(id=int(resource_id)) + + if not isinstance(resource.get_real_instance(), (Dataset, Map)): + return Response( + {"message": "Endpoint only available for Datasets and Maps.", "success": False}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Check if thumbnail exists + if not resource.thumbnail_url: + return Response( + {"message": "The thumbnail URL field is already empty.", "success": False}, + status=status.HTTP_200_OK + ) + + # request_body = request.data if request.data else json.loads(request.body) + thumb_parsed_url = urlparse(resource.thumbnail_url) + thumb_filename = thumb_parsed_url.path.split("/")[-1] + # remove_thumb will call the thumb_path function and then the storage_manager which will delete it + remove_thumb(thumb_filename) + + # Clear the field in the database + resource.thumbnail_url = "" + resource.save(update_fields=["thumbnail_url"]) + + return Response( + {"message": "Thumbnail deleted successfully.", "success": True}, status=status.HTTP_200_OK + ) + + except ResourceBase.DoesNotExist: + return Response({"message": "Resource not found.", "success": False}, status=status.HTTP_404_NOT_FOUND) + except ValueError: + return Response({"message": "Invalid resource ID.", "success": False}, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.error(e) + return Response({"message": "Unexpected error occurred.", "success": False}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @extend_schema( methods=["post"], responses={200}, description="Instructs the Async dispatcher to execute a 'INGEST' operation." ) From 72b8e1c53c872e125317ddbe4c462e4830a1ab88 Mon Sep 17 00:00:00 2001 From: gpetrak Date: Fri, 4 Apr 2025 10:02:11 +0300 Subject: [PATCH 2/4] update the delete_thumbnail endpoint --- geonode/base/api/views.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/geonode/base/api/views.py b/geonode/base/api/views.py index 9749d5aee48..12fed6d4f38 100644 --- a/geonode/base/api/views.py +++ b/geonode/base/api/views.py @@ -733,48 +733,45 @@ def set_thumbnail_from_bbox(self, request, resource_id, *args, **kwargs): methods=["post"], permission_classes=[IsAuthenticated, UserHasPerms(perms_dict={"default": {"POST": ["base.add_resourcebase"]}})], ) - def delete_thumbnail(self, request, resource_id, *args, **kwargs): - + try: resource = ResourceBase.objects.get(id=int(resource_id)) - if not isinstance(resource.get_real_instance(), (Dataset, Map)): - return Response( - {"message": "Endpoint only available for Datasets and Maps.", "success": False}, - status=status.HTTP_400_BAD_REQUEST - ) - # Check if thumbnail exists if not resource.thumbnail_url: return Response( {"message": "The thumbnail URL field is already empty.", "success": False}, - status=status.HTTP_200_OK + status=status.HTTP_200_OK, ) - # request_body = request.data if request.data else json.loads(request.body) thumb_parsed_url = urlparse(resource.thumbnail_url) thumb_filename = thumb_parsed_url.path.split("/")[-1] + if thumb_filename.rsplit(".")[-1] not in ["png", "jpeg", "jpg"]: + return Response( + "The file name is not a valid image with a format png, jpeg or jpg", + status=status.HTTP_400_BAD_REQUEST, + ) + # remove_thumb will call the thumb_path function and then the storage_manager which will delete it remove_thumb(thumb_filename) - # Clear the field in the database - resource.thumbnail_url = "" - resource.save(update_fields=["thumbnail_url"]) + # Clear the related fields in the database + resource.thumbnail_url = None + resource.thumbnail_path = None + resource.save(update_fields=["thumbnail_url", "thumbnail_path"]) - return Response( - {"message": "Thumbnail deleted successfully.", "success": True}, status=status.HTTP_200_OK - ) + return Response({"message": "Thumbnail deleted successfully.", "success": True}, status=status.HTTP_200_OK) except ResourceBase.DoesNotExist: return Response({"message": "Resource not found.", "success": False}, status=status.HTTP_404_NOT_FOUND) - except ValueError: - return Response({"message": "Invalid resource ID.", "success": False}, status=status.HTTP_400_BAD_REQUEST) except Exception as e: logger.error(e) - return Response({"message": "Unexpected error occurred.", "success": False}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + return Response( + {"message": "Unexpected error occurred.", "success": False}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) - @extend_schema( methods=["post"], responses={200}, description="Instructs the Async dispatcher to execute a 'INGEST' operation." ) From 6359fb5342b6fa26fe1373e35051507d8bb68106 Mon Sep 17 00:00:00 2001 From: gpetrak Date: Wed, 9 Apr 2025 11:37:05 +0300 Subject: [PATCH 3/4] adding a test for the delete_thumbnail endpoint --- geonode/base/api/tests.py | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index f6c452e11cb..df5839af7eb 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -1998,6 +1998,56 @@ def test_set_resource_thumbnail(self): ) self.assertEqual(response.status_code, 200) + @patch("geonode.base.api.views.remove_thumb") + def test_delete_thumbnail(self, mock_remove_thumb): + + resource = Dataset.objects.first() + + # Set a thumbnail url and path + resource.thumbnail_url = "http://example.com/thumb/test.png" + resource.thumbnail_path = "thumb/test.png" + resource.save() + + url = reverse("base-resources-delete-thumbnail", kwargs={"resource_id": resource.id}) + + # Anonymous user + response = self.client.post(url) + self.assertEqual(response.status_code, 403) + + # Authenticated user (admin) + self.assertTrue(self.client.login(username="admin", password="admin")) + + # Valid thumbnail removal + response = self.client.post(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["message"], "Thumbnail deleted successfully.") + resource.refresh_from_db() + self.assertIsNone(resource.thumbnail_url) + self.assertIsNone(resource.thumbnail_path) + mock_remove_thumb.assert_called_once_with("test.png") + + # Thumbnail already deleted + response = self.client.post(url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data["message"], "The thumbnail URL field is already empty.") + self.assertFalse(response.data["success"]) + + # Invalid image format + resource.thumbnail_url = "http://example.com/media/thumb/test.txt" + resource.thumbnail_path = "thumb/test.txt" + resource.save() + + response = self.client.post(url) + self.assertEqual(response.status_code, 400) + self.assertIn("not a valid image", response.data) + + # Resource does not exist + invalid_url = reverse("base-resources-delete-thumbnail", kwargs={"resource_id": 9999}) + response = self.client.post(invalid_url) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.data["message"], "Resource not found.") + self.assertFalse(response.data["success"]) + def test_set_thumbnail_from_bbox_from_Anonymous_user_raise_permission_error(self): """ Given a request with Anonymous user, should raise an authentication error. From 90b6cf2d74ade2a57575de9991172dbaf07ebf31 Mon Sep 17 00:00:00 2001 From: gpetrak Date: Wed, 9 Apr 2025 11:39:01 +0300 Subject: [PATCH 4/4] black re-format the tests --- geonode/base/api/tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/geonode/base/api/tests.py b/geonode/base/api/tests.py index df5839af7eb..0f9597d1e08 100644 --- a/geonode/base/api/tests.py +++ b/geonode/base/api/tests.py @@ -2000,16 +2000,16 @@ def test_set_resource_thumbnail(self): @patch("geonode.base.api.views.remove_thumb") def test_delete_thumbnail(self, mock_remove_thumb): - + resource = Dataset.objects.first() - + # Set a thumbnail url and path resource.thumbnail_url = "http://example.com/thumb/test.png" resource.thumbnail_path = "thumb/test.png" resource.save() url = reverse("base-resources-delete-thumbnail", kwargs={"resource_id": resource.id}) - + # Anonymous user response = self.client.post(url) self.assertEqual(response.status_code, 403) @@ -2047,7 +2047,7 @@ def test_delete_thumbnail(self, mock_remove_thumb): self.assertEqual(response.status_code, 404) self.assertEqual(response.data["message"], "Resource not found.") self.assertFalse(response.data["success"]) - + def test_set_thumbnail_from_bbox_from_Anonymous_user_raise_permission_error(self): """ Given a request with Anonymous user, should raise an authentication error.