Skip to content

Commit e377f59

Browse files
authored
[Fixes #8784] Implement recalculation of Geoserver featuretype and coverage bbox and update of dataset bbox (#13768)
* adding an endpoint and a helper for the bbox recalculation of GeoServer and GeoNode * move the part of REST in the geoserver-restconfig library * using _to_compact_perms to check user perms
1 parent 140fa43 commit e377f59

File tree

3 files changed

+187
-2
lines changed

3 files changed

+187
-2
lines changed

geonode/layers/api/views.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
from geonode.metadata.multilang.views import MultiLangViewMixin
3737
from geonode.resource.utils import update_resource
3838
from geonode.resource.manager import resource_manager
39-
from rest_framework.exceptions import NotFound
39+
from geonode.security.registry import permissions_registry
40+
from geonode.security.permissions import _to_compact_perms
41+
42+
from rest_framework.exceptions import NotFound, PermissionDenied
4043
from django.shortcuts import get_object_or_404
4144
from django.http import JsonResponse
4245

@@ -292,3 +295,28 @@ def timeseries_info(self, request, pk, *args, **kwards):
292295
layer.save()
293296

294297
return JsonResponse({"message": "The time dimension information for this layer was disabled"})
298+
299+
@action(
300+
detail=True,
301+
methods=["put"],
302+
url_path="recalc-bbox",
303+
url_name="recalc_bbox",
304+
permission_classes=[IsAuthenticated],
305+
)
306+
def recalc_bbox(self, request, pk=None, *args, **kwargs):
307+
dataset = self.get_object()
308+
309+
# Permissions check
310+
access = _to_compact_perms(permissions_registry.get_perms(instance=dataset, user=request.user))
311+
if access not in ("owner", "edit", "manage"):
312+
raise PermissionDenied("You do not have permission to edit this dataset")
313+
314+
# Safely get bbox from request
315+
force_bbox = getattr(request, "data", {}) or {}
316+
force_bbox = force_bbox.get("bbox", None)
317+
318+
success = dataset.recalc_bbox_on_geoserver(force_bbox=force_bbox)
319+
320+
if success:
321+
return Response({"success": True})
322+
return Response({"success": False}, status=500)

geonode/layers/models.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import logging
2222

2323
from django.conf import settings
24-
from django.db import models
24+
from django.db import models, transaction
2525
from django.urls import reverse
2626
from django.utils.timezone import now
2727
from django.utils.functional import classproperty
@@ -306,6 +306,57 @@ def attribute_config(self):
306306

307307
return cfg
308308

309+
def recalc_bbox_on_geoserver(self, force_bbox=None):
310+
"""
311+
Delegate BBOX recalculation/update to the GeoServer layer object,
312+
then refresh the Dataset's bbox fields from the updated resource.
313+
314+
This wraps the Layer.recalc_bbox() method (works for both raster + vector).
315+
"""
316+
317+
from geonode.geoserver.helpers import gs_catalog
318+
319+
# GeoServer layer object (has .resource)
320+
gs_layer = gs_catalog.get_layer(self.name)
321+
if gs_layer is None:
322+
logger.error(f"GeoServer layer not found: {self.name}")
323+
return False
324+
325+
# Call recalc_bbox method from the geoserver-restconfig library
326+
ok = gs_layer.recalc_bbox(force_bbox=force_bbox)
327+
if not ok:
328+
logger.error(f"GeoServer refused bbox update for layer {self.name}")
329+
return False
330+
331+
# Let's reset the connections first
332+
gs_catalog._cache.clear()
333+
gs_catalog.reset()
334+
# Fetch the updated resource again from GeoServer
335+
resource = gs_catalog.get_resource(
336+
name=self.name, store=gs_layer.resource.store, workspace=gs_layer.resource.workspace
337+
)
338+
339+
if not resource:
340+
logger.error("No resource returned from GeoServer after bbox update")
341+
return False
342+
343+
bbox = resource.native_bbox
344+
ll = resource.latlon_bbox
345+
srid = resource.projection
346+
347+
if not bbox or not ll:
348+
logger.error("GeoServer did not return updated bbox values")
349+
return False
350+
351+
# bbox order from GeoServer: [minx, maxx, miny, maxy]
352+
with transaction.atomic():
353+
self.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], srid)
354+
self.set_ll_bbox_polygon([ll[0], ll[2], ll[1], ll[3]])
355+
self.srid = srid or self.srid
356+
self.save(update_fields=["srid"])
357+
358+
return True
359+
309360
def __str__(self):
310361
return str(self.alternate)
311362

geonode/layers/tests.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
from unittest.mock import MagicMock, patch, PropertyMock
2727
from collections import namedtuple
2828

29+
from rest_framework.test import force_authenticate
30+
from rest_framework import status
31+
2932
from django.urls import reverse
3033
from django.test import TestCase
3134
from django.test.client import RequestFactory
@@ -799,6 +802,109 @@ def test_supports_time_with_raster_subtype_and_invalid_attributes(self, mock_get
799802
mock_dataset = Dataset(subtype="raster")
800803
self.assertFalse(mock_dataset.supports_time)
801804

805+
@patch("geonode.geoserver.helpers.gs_catalog.get_resource")
806+
@patch("geonode.geoserver.helpers.gs_catalog.get_layer")
807+
def test_dataset_recalc_bbox_on_geoserver_success(self, mock_get_layer, mock_get_resource):
808+
"""
809+
Test recalc_bbox_on_geoserver() without touching real GeoServer.
810+
"""
811+
812+
# Mock the GeoServer Layer object
813+
mock_layer = MagicMock()
814+
mock_layer.resource.store = MagicMock(name="test_store", workspace=MagicMock(name="test_workspace"))
815+
# Mock recalc_bbox() returning True
816+
mock_layer.recalc_bbox.return_value = True
817+
mock_get_layer.return_value = mock_layer
818+
819+
# Mock the resource returned by gs_catalog.get_resource
820+
mock_resource = MagicMock()
821+
mock_resource.native_bbox = [0, 10, 0, 10]
822+
mock_resource.latlon_bbox = [0, 10, 0, 10]
823+
mock_resource.projection = "EPSG:4326"
824+
mock_resource.store = mock_layer.resource.store
825+
mock_get_resource.return_value = mock_resource
826+
827+
# Call the method
828+
result = self.dataset.recalc_bbox_on_geoserver(force_bbox=[0, 0, 10, 10])
829+
830+
# Assertions
831+
self.assertTrue(result)
832+
mock_layer.recalc_bbox.assert_called_once_with(force_bbox=[0, 0, 10, 10])
833+
mock_get_resource.assert_called_once_with(
834+
name=self.dataset.name, store=mock_layer.resource.store, workspace=mock_layer.resource.workspace
835+
)
836+
837+
# Check if bbox and srid are updated correctly
838+
self.assertEqual(self.dataset.srid, "EPSG:4326")
839+
# Optionally, check polygons
840+
self.assertIsNotNone(self.dataset.bbox_polygon)
841+
self.assertIsNotNone(self.dataset.ll_bbox_polygon)
842+
843+
@patch("geonode.layers.models.Dataset.recalc_bbox_on_geoserver")
844+
def test_recalc_bbox_view_success(self, mock_recalc_bbox):
845+
"""Test the recalc_bbox view returns 200 when successful."""
846+
factory = RequestFactory()
847+
django_request = factory.put(
848+
f"/api/v2/datasets/{self.dataset.id}/recalc-bbox/",
849+
data='{"bbox": [0, 0, 10, 10]}',
850+
content_type="application/json",
851+
)
852+
force_authenticate(django_request, user=self.admin_user) # authenticate the HttpRequest
853+
854+
mock_recalc_bbox.return_value = True
855+
856+
from geonode.layers.api.views import DatasetViewSet
857+
858+
view = DatasetViewSet.as_view({"put": "recalc_bbox"})
859+
response = view(django_request, pk=self.dataset.id) # pass HttpRequest directly
860+
861+
assert response.status_code == status.HTTP_200_OK
862+
assert response.data == {"success": True}
863+
mock_recalc_bbox.assert_called_once_with(force_bbox=[0, 0, 10, 10])
864+
865+
@patch("geonode.layers.models.Dataset.recalc_bbox_on_geoserver")
866+
def test_recalc_bbox_view_failure(self, mock_recalc_bbox):
867+
"""Test the recalc_bbox view returns 500 when GeoServer update fails."""
868+
factory = RequestFactory()
869+
django_request = factory.put(
870+
f"/api/v2/datasets/{self.dataset.id}/recalc-bbox/",
871+
data='{"bbox": [0, 0, 10, 10]}',
872+
content_type="application/json",
873+
)
874+
force_authenticate(django_request, user=self.admin_user)
875+
876+
mock_recalc_bbox.return_value = False
877+
878+
from geonode.layers.api.views import DatasetViewSet
879+
880+
view = DatasetViewSet.as_view({"put": "recalc_bbox"})
881+
response = view(django_request, pk=self.dataset.id)
882+
883+
assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
884+
assert response.data == {"success": False}
885+
mock_recalc_bbox.assert_called_once_with(force_bbox=[0, 0, 10, 10])
886+
887+
@patch("geonode.layers.models.Dataset.recalc_bbox_on_geoserver")
888+
def test_recalc_bbox_view_permission_denied(self, mock_recalc_bbox):
889+
"""Test the recalc_bbox view denies access if user lacks edit perms."""
890+
factory = RequestFactory()
891+
django_request = factory.put(
892+
f"/api/v2/datasets/{self.dataset.id}/recalc-bbox/",
893+
data='{"bbox": [0, 0, 10, 10]}',
894+
content_type="application/json",
895+
)
896+
force_authenticate(django_request, user=self.anonymous_user) # no edit perms
897+
898+
from geonode.layers.api.views import DatasetViewSet
899+
900+
view = DatasetViewSet.as_view({"put": "recalc_bbox"})
901+
902+
response = view(django_request, pk=self.dataset.id)
903+
904+
# DRF converts PermissionDenied to HTTP 403 response
905+
assert response.status_code == 403
906+
mock_recalc_bbox.assert_not_called()
907+
802908

803909
class TestLayerDetailMapViewRights(GeoNodeBaseTestSupport):
804910
fixtures = ["initial_data.json", "group_test_data.json", "default_oauth_apps.json"]

0 commit comments

Comments
 (0)