Skip to content

Commit

Permalink
Merge branch 'master' of github.com:GeoNode/geonode into ISSUE_12828
Browse files Browse the repository at this point in the history
  • Loading branch information
mattiagiupponi committed Jan 24, 2025
2 parents f255031 + 0258939 commit e78deb6
Show file tree
Hide file tree
Showing 16 changed files with 402 additions and 43 deletions.
31 changes: 31 additions & 0 deletions geonode/geoserver/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1808,6 +1808,37 @@ def set_time_info(layer, attribute, end_attribute, presentation, precision_value
gs_catalog.save(resource)


def get_time_info(layer):
"""
Get the time configuration for a layer
"""
time_info = {}
gs_layer = gs_catalog.get_layer(name=layer.name)
if gs_layer is not None:
gs_time_info = gs_layer.resource.metadata.get("time")
if gs_time_info.enabled:
_attr = layer.attributes.filter(attribute=gs_time_info.attribute).first()
time_info["attribute"] = _attr.pk if _attr else None
if gs_time_info.end_attribute is not None:
end_attr = layer.attributes.filter(attribute=gs_time_info.end_attribute).first()
time_info["end_attribute"] = end_attr.pk if end_attr else None
time_info["presentation"] = gs_time_info.presentation
lookup_value = sorted(list(gs_time_info._lookup), key=lambda x: x[1], reverse=True)
if gs_time_info.resolution is not None:
res = gs_time_info.resolution // 1000
for el in lookup_value:
if res % el[1] == 0:
time_info["precision_value"] = res // el[1]
time_info["precision_step"] = el[0]
break
else:
time_info["precision_value"] = gs_time_info.resolution
time_info["precision_step"] = "seconds"
return time_info
else:
return None


ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"]

_wms = None
Expand Down
87 changes: 86 additions & 1 deletion geonode/geoserver/tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,21 @@
from geonode.layers.populate_datasets_data import create_dataset_data
from geonode.tests.base import GeoNodeBaseTestSupport
from geonode.geoserver.views import _response_callback
from geonode.layers.models import Dataset, Attribute
from uuid import uuid4
from django.contrib.auth import get_user_model

from geonode.geoserver.helpers import (
gs_catalog,
ows_endpoint_in_path,
get_dataset_storetype,
extract_name_from_sld,
get_dataset_capabilities_url,
get_layer_ows_url,
get_time_info,
)
from geonode.geoserver.ows import _wcs_link, _wfs_link, _wms_link
from unittest.mock import patch, Mock


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -267,11 +273,90 @@ def test_dataset_capabilties_url(self):

@on_ogc_backend(geoserver.BACKEND_PACKAGE)
def test_layer_ows_url(self):
from geonode.layers.models import Dataset

ows_url = settings.GEOSERVER_PUBLIC_LOCATION
identifier = "geonode:CA"
dataset = Dataset.objects.get(alternate=identifier)
expected_url = f"{ows_url}geonode/CA/ows"
capabilities_url = get_layer_ows_url(dataset)
self.assertEqual(capabilities_url, expected_url, capabilities_url)

# Tests for geonode.geoserver.helpers.get_time_info
@patch("geonode.geoserver.helpers.gs_catalog")
def test_get_time_info_valid_layer(self, mock_gs_catalog):

mock_dataset = Dataset.objects.create(
uuid=str(uuid4()),
owner=get_user_model().objects.get(username=self.user),
name="geonode:states",
store="httpfooremoteservce",
subtype="remote",
alternate="geonode:states",
)

Attribute.objects.create(pk=5, attribute="begin", dataset_id=mock_dataset.pk)

Attribute.objects.create(pk=6, attribute="end", dataset_id=mock_dataset.pk)

# Build mock GeoServer's time info
mock_gs_time_info = Mock()
mock_gs_time_info.enabled = True
mock_gs_time_info.attribute = "begin"
mock_gs_time_info.end_attribute = "end"
mock_gs_time_info.presentation = "DISCRETE_INTERVAL"
mock_gs_time_info.resolution = 5000
mock_gs_time_info._lookup = [("seconds", 1), ("minutes", 60)]

mock_gs_layer = Mock()
mock_gs_layer.resource.metadata.get.return_value = mock_gs_time_info
mock_gs_catalog.get_layer.return_value = mock_gs_layer

result = get_time_info(mock_dataset)

self.assertEqual(result["attribute"], 5)
self.assertEqual(result["end_attribute"], 6)
self.assertEqual(result["presentation"], "DISCRETE_INTERVAL")
self.assertEqual(result["precision_value"], 5)
self.assertEqual(result["precision_step"], "seconds")

@patch("geonode.geoserver.helpers.gs_catalog")
def test_get_time_info_with_time_disabled(self, mock_gs_catalog):

mock_dataset = Dataset.objects.create(
uuid=str(uuid4()),
owner=get_user_model().objects.get(username=self.user),
name="geonode:states",
store="httpfooremoteservce",
subtype="remote",
alternate="geonode:states",
)

Attribute.objects.create(pk=5, attribute="begin", dataset_id=mock_dataset.pk)

Attribute.objects.create(pk=6, attribute="end", dataset_id=mock_dataset.pk)

mock_gs_time_info = Mock()
mock_gs_time_info.enabled = False
mock_gs_time_info.attribute = "begin"
mock_gs_time_info.end_attribute = "end"
mock_gs_time_info.presentation = "DISCRETE_INTERVAL"
mock_gs_time_info.resolution = 10000
mock_gs_time_info._lookup = [("seconds", 1), ("minutes", 60)]

mock_gs_layer = Mock()
mock_gs_layer.resource.metadata.get.return_value = mock_gs_time_info
mock_gs_catalog.get_layer.return_value = mock_gs_layer

result = get_time_info(mock_dataset)
self.assertEqual(result, {})

@patch("geonode.geoserver.helpers.gs_catalog")
def test_get_time_info_no_layer(self, mock_gs_catalog):

mock_gs_catalog.get_layer.return_value = None

mock_layer = Mock()
mock_layer.name = "nonexistent_layer"

result = get_time_info(mock_layer)
self.assertIsNone(result)
37 changes: 37 additions & 0 deletions geonode/layers/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,40 @@ class DatasetMetadataSerializer(serializers.Serializer):

class Meta:
fields = "metadata_file"


class DatasetTimeSeriesSerializer(serializers.Serializer):

def __init__(self, *args, **kwargs):

super().__init__(*args, **kwargs)

layer = self.context.get("layer")

if layer:
# use the get_choices method of the Dataset model
choices = [(None, "-----")] + layer.get_choices
self.fields["attribute"].choices = choices
self.fields["end_attribute"].choices = choices
else:
choices = [(None, "-----")]

has_time = serializers.BooleanField(default=False)
attribute = serializers.ChoiceField(choices=[], required=False)
end_attribute = serializers.ChoiceField(choices=[], required=False)
presentation = serializers.ChoiceField(
required=False,
choices=[
("LIST", "List of all the distinct time values"),
("DISCRETE_INTERVAL", "Intervals defined by the resolution"),
(
"CONTINUOUS_INTERVAL",
"Continuous Intervals for data that is frequently updated, resolution describes the frequency of updates",
),
],
)
precision_value = serializers.IntegerField(required=False)
precision_step = serializers.ChoiceField(
required=False,
choices=[("years",) * 2, ("months",) * 2, ("days",) * 2, ("hours",) * 2, ("minutes",) * 2, ("seconds",) * 2],
)
106 changes: 106 additions & 0 deletions geonode/layers/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,25 @@
from geonode.resource.utils import update_resource
from geonode.resource.manager import resource_manager
from rest_framework.exceptions import NotFound
from django.shortcuts import get_object_or_404
from django.http import JsonResponse

from geonode.storage.manager import StorageManager

from .serializers import (
DatasetSerializer,
DatasetListSerializer,
DatasetMetadataSerializer,
DatasetTimeSeriesSerializer,
)
from .permissions import DatasetPermissionsFilter

from geonode import geoserver
from geonode.utils import check_ogc_backend

if check_ogc_backend(geoserver.BACKEND_PACKAGE):
from geonode.geoserver.helpers import get_time_info

import logging

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -80,6 +89,8 @@ class DatasetViewSet(ApiPresetsInitializer, DynamicModelViewSet, AdvertisedListM
def get_serializer_class(self):
if self.action == "list":
return DatasetListSerializer
if self.action == "timeseries_info":
return DatasetTimeSeriesSerializer
return DatasetSerializer

def partial_update(self, request, *args, **kwargs):
Expand Down Expand Up @@ -187,3 +198,98 @@ def maps(self, request, pk=None, *args, **kwargs):
dataset = self.get_object()
resources = dataset.maps
return Response(SimpleMapSerializer(many=True).to_representation(resources))

@action(
detail=True,
url_path="timeseries",
url_name="timeseries",
methods=["get", "put"],
permission_classes=[IsAuthenticated],
)
def timeseries_info(self, request, pk, *args, **kwards):
"""
Endpoint for timeseries information
url = "http://localhost:8080/api/v2/datasets/{dataset_id}/timeseries"
cURL examples:
GET method
curl -X GET http://localhost:8000/api/v2/datasets/1/timeseries -u <username>:<password>
PUT method
curl -X PUT http://localhost:8000/api/v2/datasets/1/timeseries -u <username>:<password>
-H "Content-Type: application/json" -d '{"has_time": true, "attribute": 4, "end_attribute": 5,
"presentation": "DISCRETE_INTERVAL", "precision_value": 2, "precision_step": "months"}'
"""

layer = get_object_or_404(Dataset, id=pk)

if layer.supports_time is False:
return JsonResponse({"message": "The time dimension is not supported for this dataset."}, status=200)

if request.method == "GET":

time_info = get_time_info(layer)
serializer = DatasetTimeSeriesSerializer(data=time_info, context={"layer": layer})
serializer.is_valid(raise_exception=True)
serialized_time_info = serializer.data

if layer.has_time is True and time_info is not None:
serialized_time_info["has_time"] = layer.has_time
return JsonResponse(serialized_time_info, status=200)
else:
return JsonResponse({"message": "No time information available."}, status=404)

if request.method == "PUT":

serializer = DatasetTimeSeriesSerializer(data=request.data, context={"layer": layer})
serializer.is_valid(raise_exception=True)
serialized_time_info = serializer.validated_data

if serialized_time_info.get("has_time") is True:

start_attr = (
layer.attributes.get(pk=serialized_time_info.get("attribute")).attribute
if serialized_time_info.get("attribute")
else None
)
end_attr = (
layer.attributes.get(pk=serialized_time_info.get("end_attribute")).attribute
if serialized_time_info.get("end_attribute")
else None
)

# Save the has_time value to the database
layer.has_time = True
layer.save()

resource_manager.exec(
"set_time_info",
None,
instance=layer,
time_info={
"attribute": start_attr,
"end_attribute": end_attr,
"presentation": serialized_time_info.get("presentation", None),
"precision_value": serialized_time_info.get("precision_value", None),
"precision_step": serialized_time_info.get("precision_step", None),
"enabled": serialized_time_info.get("has_time", False),
},
)

resource_manager.update(
layer.uuid,
instance=layer,
notify=True,
)
return JsonResponse({"message": "the time information data was updated successfully"}, status=200)
else:
# Save the has_time value to the database
layer.has_time = False
layer.save()

return JsonResponse(
{
"message": "The time information was not updated since the time dimension is disabled for this layer"
}
)
15 changes: 15 additions & 0 deletions geonode/layers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,21 @@ def is_vector(self):
def is_raster(self):
return self.subtype == "raster"

@property
def supports_time(self):
valid_attributes = self.get_choices
# check if the layer object if a vector and
# includes valid_attributes
if self.is_vector() and valid_attributes:
return True
return False

@property
def get_choices(self):

attributes = Attribute.objects.filter(dataset_id=self.pk)
return [(_a.pk, _a.attribute) for _a in attributes if _a.attribute_type in ["xsd:dateTime", "xsd:date"]]

@property
def display_type(self):
if self.subtype in ["vector", "vector_time"]:
Expand Down
Loading

0 comments on commit e78deb6

Please sign in to comment.