From 97efacf5498dea4136e75d5a13fabb4e45604a0d Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Fri, 13 Feb 2026 06:21:37 +0000 Subject: [PATCH 1/6] [Fixes #13952] Handle the exception when the update bbox method fails at managing the SRID for a dataset --- geonode/geoserver/helpers.py | 11 +---------- geonode/layers/models.py | 27 +++++++++++++++++++++++---- geonode/layers/tests.py | 25 +++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index b3a1ff73fe2..5e893a7a288 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -2028,16 +2028,7 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): srid = gs_resource.projection bbox = gs_resource.native_bbox ll_bbox = gs_resource.latlon_bbox - try: - instance.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], srid) - except GeoNodeException as e: - if not ll_bbox: - raise - else: - logger.exception(e) - instance.srid = "EPSG:4326" - Dataset.objects.filter(id=instance.id).update(srid=instance.srid) - instance.set_ll_bbox_polygon([ll_bbox[0], ll_bbox[2], ll_bbox[1], ll_bbox[3]]) + instance.set_bbox_and_srid_from_geoserver(bbox=bbox, ll_bbox=ll_bbox, srid=srid) if instance.srid: instance.srid_url = ( diff --git a/geonode/layers/models.py b/geonode/layers/models.py index f4f406dddd9..498794baf63 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -30,6 +30,7 @@ from tinymce.models import HTMLField from geonode.client.hooks import hookset +from geonode import GeoNodeException from geonode.utils import build_absolute_uri, check_shp_columnnames from geonode.security.models import PermissionLevelMixin from geonode.groups.conf import settings as groups_settings @@ -348,15 +349,33 @@ def recalc_bbox_on_geoserver(self, force_bbox=None): logger.error("GeoServer did not return updated bbox values") return False - # bbox order from GeoServer: [minx, maxx, miny, maxy] with transaction.atomic(): - self.set_bbox_polygon([bbox[0], bbox[2], bbox[1], bbox[3]], srid) - self.set_ll_bbox_polygon([ll[0], ll[2], ll[1], ll[3]]) - self.srid = srid or self.srid + self.set_bbox_and_srid_from_geoserver(bbox=bbox, ll_bbox=ll, srid=srid) self.save(update_fields=["srid"]) return True + def set_bbox_and_srid_from_geoserver(self, bbox, ll_bbox, srid): + if not ll_bbox or len(ll_bbox) < 4: + raise GeoNodeException("GeoServer did not return latlon bbox") + + ll_bbox_gn = [ll_bbox[0], ll_bbox[2], ll_bbox[1], ll_bbox[3]] + # Try to use native bbox if available + if bbox and not len(bbox) < 4 and srid: + try: + native_bbox_gn = [bbox[0], bbox[2], bbox[1], bbox[3]] + self.set_bbox_polygon(native_bbox_gn, srid) + self.set_ll_bbox_polygon(ll_bbox_gn) + self.srid = srid + return + except GeoNodeException as e: + logger.warning(f"Failed to set native bbox with SRID {srid}, falling back to EPSG:4326: {e}") + + # Fallback to lat/lon bbox + self.set_bbox_polygon(ll_bbox_gn, "EPSG:4326") + self.set_ll_bbox_polygon(ll_bbox_gn) + self.srid = "EPSG:4326" + def __str__(self): return str(self.alternate) diff --git a/geonode/layers/tests.py b/geonode/layers/tests.py index 16a55de4f9b..3168d20f8d7 100644 --- a/geonode/layers/tests.py +++ b/geonode/layers/tests.py @@ -817,6 +817,31 @@ def test_dataset_recalc_bbox_on_geoserver_success(self, mock_get_layer, mock_get self.assertIsNotNone(self.dataset.bbox_polygon) self.assertIsNotNone(self.dataset.ll_bbox_polygon) + @patch("geonode.base.models.bbox_to_projection", side_effect=Exception("Unsupported CRS")) + def test_set_bbox_and_srid_from_geoserver_fallback_to_ll_bbox(self, _mock_bbox_to_projection): + """ + If native bbox reprojection fails, fallback to EPSG:4326 and ll_bbox for both polygons. + """ + native_bbox = [1535760, 1786050, 4652670, 5126620] + ll_bbox = [8.7, 9.2, 45.1, 45.5] + + self.dataset.set_bbox_and_srid_from_geoserver(bbox=native_bbox, ll_bbox=ll_bbox, srid="EPSG:3003") + + self.assertEqual(self.dataset.srid, "EPSG:4326") + self.assertIsNotNone(self.dataset.bbox_polygon) + self.assertIsNotNone(self.dataset.ll_bbox_polygon) + self.assertEqual(self.dataset.bbox_polygon.wkt, self.dataset.ll_bbox_polygon.wkt) + + def test_set_bbox_and_srid_from_geoserver_uses_native_bbox_when_supported(self): + native_bbox = [0, 10, 0, 10] + ll_bbox = [0, 10, 0, 10] + + self.dataset.set_bbox_and_srid_from_geoserver(bbox=native_bbox, ll_bbox=ll_bbox, srid="EPSG:4326") + + self.assertEqual(self.dataset.srid, "EPSG:4326") + self.assertIsNotNone(self.dataset.bbox_polygon) + self.assertIsNotNone(self.dataset.ll_bbox_polygon) + @patch("geonode.layers.models.Dataset.recalc_bbox_on_geoserver") def test_recalc_bbox_view_success(self, mock_recalc_bbox): """Test the recalc_bbox view returns 200 when successful.""" From 796343cadf61d58eaba1982051951d574e870bea Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Fri, 13 Feb 2026 06:55:59 +0000 Subject: [PATCH 2/6] [Fixes #13952] added code assist suggestion --- geonode/layers/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geonode/layers/models.py b/geonode/layers/models.py index 498794baf63..93540514cac 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -361,7 +361,7 @@ def set_bbox_and_srid_from_geoserver(self, bbox, ll_bbox, srid): ll_bbox_gn = [ll_bbox[0], ll_bbox[2], ll_bbox[1], ll_bbox[3]] # Try to use native bbox if available - if bbox and not len(bbox) < 4 and srid: + if bbox and len(bbox) >= 4 and srid: try: native_bbox_gn = [bbox[0], bbox[2], bbox[1], bbox[3]] self.set_bbox_polygon(native_bbox_gn, srid) From a57fd45e32011f810d17f25c250b70dd681d6bbc Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Tue, 17 Feb 2026 07:14:14 +0000 Subject: [PATCH 3/6] [Fixes #13952] updated the method name to generic method name --- geonode/geoserver/helpers.py | 2 +- geonode/layers/models.py | 8 ++++---- geonode/layers/tests.py | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index 5e893a7a288..c049543942b 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -2028,7 +2028,7 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): srid = gs_resource.projection bbox = gs_resource.native_bbox ll_bbox = gs_resource.latlon_bbox - instance.set_bbox_and_srid_from_geoserver(bbox=bbox, ll_bbox=ll_bbox, srid=srid) + instance.set_bbox_and_srid(bbox=bbox, ll_bbox=ll_bbox, srid=srid) if instance.srid: instance.srid_url = ( diff --git a/geonode/layers/models.py b/geonode/layers/models.py index 93540514cac..c1b1b9a96ba 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -350,14 +350,14 @@ def recalc_bbox_on_geoserver(self, force_bbox=None): return False with transaction.atomic(): - self.set_bbox_and_srid_from_geoserver(bbox=bbox, ll_bbox=ll, srid=srid) + self.set_bbox_and_srid(bbox=bbox, ll_bbox=ll, srid=srid) self.save(update_fields=["srid"]) return True - def set_bbox_and_srid_from_geoserver(self, bbox, ll_bbox, srid): + def set_bbox_and_srid(self, bbox, ll_bbox, srid): if not ll_bbox or len(ll_bbox) < 4: - raise GeoNodeException("GeoServer did not return latlon bbox") + raise GeoNodeException("Lat/Lon BBox was not provided") ll_bbox_gn = [ll_bbox[0], ll_bbox[2], ll_bbox[1], ll_bbox[3]] # Try to use native bbox if available @@ -369,7 +369,7 @@ def set_bbox_and_srid_from_geoserver(self, bbox, ll_bbox, srid): self.srid = srid return except GeoNodeException as e: - logger.warning(f"Failed to set native bbox with SRID {srid}, falling back to EPSG:4326: {e}") + logger.warning(f"Failed to set bbox with SRID {srid}, falling back to EPSG:4326: {e}") # Fallback to lat/lon bbox self.set_bbox_polygon(ll_bbox_gn, "EPSG:4326") diff --git a/geonode/layers/tests.py b/geonode/layers/tests.py index 3168d20f8d7..353c360a681 100644 --- a/geonode/layers/tests.py +++ b/geonode/layers/tests.py @@ -818,25 +818,25 @@ def test_dataset_recalc_bbox_on_geoserver_success(self, mock_get_layer, mock_get self.assertIsNotNone(self.dataset.ll_bbox_polygon) @patch("geonode.base.models.bbox_to_projection", side_effect=Exception("Unsupported CRS")) - def test_set_bbox_and_srid_from_geoserver_fallback_to_ll_bbox(self, _mock_bbox_to_projection): + def test_set_bbox_and_srid_fallback_to_ll_bbox(self, _mock_bbox_to_projection): """ If native bbox reprojection fails, fallback to EPSG:4326 and ll_bbox for both polygons. """ native_bbox = [1535760, 1786050, 4652670, 5126620] ll_bbox = [8.7, 9.2, 45.1, 45.5] - self.dataset.set_bbox_and_srid_from_geoserver(bbox=native_bbox, ll_bbox=ll_bbox, srid="EPSG:3003") + self.dataset.set_bbox_and_srid(bbox=native_bbox, ll_bbox=ll_bbox, srid="EPSG:3003") self.assertEqual(self.dataset.srid, "EPSG:4326") self.assertIsNotNone(self.dataset.bbox_polygon) self.assertIsNotNone(self.dataset.ll_bbox_polygon) self.assertEqual(self.dataset.bbox_polygon.wkt, self.dataset.ll_bbox_polygon.wkt) - def test_set_bbox_and_srid_from_geoserver_uses_native_bbox_when_supported(self): + def test_set_bbox_and_srid_uses_native_bbox_when_supported(self): native_bbox = [0, 10, 0, 10] ll_bbox = [0, 10, 0, 10] - self.dataset.set_bbox_and_srid_from_geoserver(bbox=native_bbox, ll_bbox=ll_bbox, srid="EPSG:4326") + self.dataset.set_bbox_and_srid(bbox=native_bbox, ll_bbox=ll_bbox, srid="EPSG:4326") self.assertEqual(self.dataset.srid, "EPSG:4326") self.assertIsNotNone(self.dataset.bbox_polygon) From 542cc677688bb8615abc0eb9c17189151b367f76 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Wed, 18 Feb 2026 11:28:15 +0000 Subject: [PATCH 4/6] [Fixes #13952] added method for bbox validity check --- geonode/layers/models.py | 8 ++++---- geonode/utils.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/geonode/layers/models.py b/geonode/layers/models.py index c1b1b9a96ba..3ea95678449 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -31,7 +31,7 @@ from geonode.client.hooks import hookset from geonode import GeoNodeException -from geonode.utils import build_absolute_uri, check_shp_columnnames +from geonode.utils import build_absolute_uri, check_shp_columnnames, check_bbox_validity from geonode.security.models import PermissionLevelMixin from geonode.groups.conf import settings as groups_settings from geonode.security.permissions import ( @@ -356,12 +356,12 @@ def recalc_bbox_on_geoserver(self, force_bbox=None): return True def set_bbox_and_srid(self, bbox, ll_bbox, srid): - if not ll_bbox or len(ll_bbox) < 4: - raise GeoNodeException("Lat/Lon BBox was not provided") + if not check_bbox_validity(ll_bbox): + raise GeoNodeException("Lat/Lon BBox was not provided or is invalid") ll_bbox_gn = [ll_bbox[0], ll_bbox[2], ll_bbox[1], ll_bbox[3]] # Try to use native bbox if available - if bbox and len(bbox) >= 4 and srid: + if check_bbox_validity(bbox) and srid: try: native_bbox_gn = [bbox[0], bbox[2], bbox[1], bbox[3]] self.set_bbox_polygon(native_bbox_gn, srid) diff --git a/geonode/utils.py b/geonode/utils.py index 315d380c83f..a7d9d2ac9b1 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -422,6 +422,17 @@ def bbox_swap(bbox): return [_bbox[0], _bbox[2], _bbox[1], _bbox[3]] +def check_bbox_validity(value): + if ( + not value + or not isinstance(value, list) + or len(value) < 4 + or not all(isinstance(x, (int, float)) for x in value) + ): + return False + return True + + def bbox_to_wkt(x0, x1, y0, y1, srid="4326", include_srid=True): if srid and str(srid).startswith("EPSG:"): srid = srid[5:] From 00fb5a4ab4488e9696f45eb74fee82c66b0b2606 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Wed, 18 Feb 2026 12:36:05 +0000 Subject: [PATCH 5/6] [Fixes #13952] fixes validation logic --- geonode/utils.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/geonode/utils.py b/geonode/utils.py index a7d9d2ac9b1..4cb303ac0a3 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -423,12 +423,11 @@ def bbox_swap(bbox): def check_bbox_validity(value): - if ( - not value - or not isinstance(value, list) - or len(value) < 4 - or not all(isinstance(x, (int, float)) for x in value) - ): + if not value or not isinstance(value, (list, tuple)) or len(value) < 4: + return False + try: + all(float(x) for x in value[:4]) + except (ValueError, TypeError): return False return True From 0022d662b25859f35859820fb148141ed66b6c39 Mon Sep 17 00:00:00 2001 From: sijandh35 Date: Thu, 19 Feb 2026 04:58:12 +0000 Subject: [PATCH 6/6] [Fixes #13952] added type and proper description for future use and modify validity --- geonode/geoserver/helpers.py | 5 +++-- geonode/layers/models.py | 25 +++++++++++++++++++++---- geonode/utils.py | 34 ++++++++++++++++++++++++++++------ 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/geonode/geoserver/helpers.py b/geonode/geoserver/helpers.py index c049543942b..e47f3274bd1 100755 --- a/geonode/geoserver/helpers.py +++ b/geonode/geoserver/helpers.py @@ -74,6 +74,7 @@ get_legend_url, is_monochromatic_image, set_resource_default_links, + normalize_bbox_to_float_list, ) from .geofence import GeoFenceClient, GeoFenceUtils @@ -2026,8 +2027,8 @@ def sync_instance_with_geoserver(instance_id, *args, **kwargs): # are bypassed by custom create/updates we need to ensure the # bbox is calculated properly. srid = gs_resource.projection - bbox = gs_resource.native_bbox - ll_bbox = gs_resource.latlon_bbox + bbox = normalize_bbox_to_float_list(gs_resource.native_bbox) + ll_bbox = normalize_bbox_to_float_list(gs_resource.latlon_bbox) instance.set_bbox_and_srid(bbox=bbox, ll_bbox=ll_bbox, srid=srid) if instance.srid: diff --git a/geonode/layers/models.py b/geonode/layers/models.py index 3ea95678449..66e77cf6357 100644 --- a/geonode/layers/models.py +++ b/geonode/layers/models.py @@ -19,6 +19,7 @@ import itertools import re import logging +from typing import List, Union from django.conf import settings from django.db import models, transaction @@ -31,7 +32,7 @@ from geonode.client.hooks import hookset from geonode import GeoNodeException -from geonode.utils import build_absolute_uri, check_shp_columnnames, check_bbox_validity +from geonode.utils import build_absolute_uri, check_shp_columnnames, check_bbox_validity, normalize_bbox_to_float_list from geonode.security.models import PermissionLevelMixin from geonode.groups.conf import settings as groups_settings from geonode.security.permissions import ( @@ -341,8 +342,8 @@ def recalc_bbox_on_geoserver(self, force_bbox=None): logger.error("No resource returned from GeoServer after bbox update") return False - bbox = resource.native_bbox - ll = resource.latlon_bbox + bbox = normalize_bbox_to_float_list(resource.native_bbox) + ll = normalize_bbox_to_float_list(resource.latlon_bbox) srid = resource.projection if not bbox or not ll: @@ -355,7 +356,23 @@ def recalc_bbox_on_geoserver(self, force_bbox=None): return True - def set_bbox_and_srid(self, bbox, ll_bbox, srid): + def set_bbox_and_srid( + self, + bbox: List[Union[int, float]], + ll_bbox: List[Union[int, float]], + srid: str, + ) -> None: + """ + Set dataset bbox, ll_bbox and spatial reference identifier. + Args: + bbox: Bounding box as [minx, maxx, miny, maxy], each element is int or float. + If invalid or unavailable, the method falls back to ll_bbox with EPSG:4326. + ll_bbox: Lat/Lon bounding box as [minx, maxx, miny, maxy], each element is int or float. + Must be valid, otherwise a GeoNodeException is raised. + srid: Spatial Reference Identifier (string). + Returns: + None + """ if not check_bbox_validity(ll_bbox): raise GeoNodeException("Lat/Lon BBox was not provided or is invalid") diff --git a/geonode/utils.py b/geonode/utils.py index 4cb303ac0a3..e750a1bd02b 100755 --- a/geonode/utils.py +++ b/geonode/utils.py @@ -46,6 +46,7 @@ from math import atan, exp, log, pi, tan from zipfile import ZipFile, is_zipfile, ZIP_DEFLATED from geonode.upload.api.exceptions import GeneralUploadException +from typing import List, Optional, Union from django.conf import settings from django.db.models import signals @@ -422,16 +423,37 @@ def bbox_swap(bbox): return [_bbox[0], _bbox[2], _bbox[1], _bbox[3]] -def check_bbox_validity(value): - if not value or not isinstance(value, (list, tuple)) or len(value) < 4: - return False - try: - all(float(x) for x in value[:4]) - except (ValueError, TypeError): +def check_bbox_validity(value: List[Union[int, float]]) -> bool: + """ + Validate that a bounding box value is a non-empty list of at least 4 numeric values. + + Args: + value: Bounding box to validate, expected as [minx, maxx, miny, maxy]. + Accepts int or float values. None is treated as invalid. + + Returns: + True if `value` is a list with at least 4 numeric (int or float) elements, False otherwise. + """ + if ( + not value + or not isinstance(value, list) + or len(value) < 4 + or not all(isinstance(x, (int, float)) for x in value) + ): return False return True +def normalize_bbox_to_float_list(value) -> Optional[List[float]]: + """Normalize bbox input to a 4-item float list or return None when invalid.""" + if not value or len(value) < 4: + return None + try: + return [float(x) for x in value[:4]] + except (TypeError, ValueError): + return None + + def bbox_to_wkt(x0, x1, y0, y1, srid="4326", include_srid=True): if srid and str(srid).startswith("EPSG:"): srid = srid[5:]