Skip to content

Commit 1a6108b

Browse files
[FIXES #12766] API for timeseries settings (#12767)
* adding timeseries API * fixing a bug * black re-formatting * Fix serializer for DatasetTimeSerie * make some small improvements * black reformatting * improving the code * formatting the code * adding get_choices under the __init__ function of the serializer * adding a support_time property * update layers/views with the support_time property * rename the property support_time to supports_time * adding a get_choices property to the Dataset model and extending the supports_time property * adding tests for the get_time_info function and for the supports_time property * fixing a bug * update the tests for the get_time_info * removing non-used module --------- Co-authored-by: Mattia <[email protected]>
1 parent cc1e8a9 commit 1a6108b

File tree

8 files changed

+336
-25
lines changed

8 files changed

+336
-25
lines changed

geonode/geoserver/helpers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1808,6 +1808,37 @@ def set_time_info(layer, attribute, end_attribute, presentation, precision_value
18081808
gs_catalog.save(resource)
18091809

18101810

1811+
def get_time_info(layer):
1812+
"""
1813+
Get the time configuration for a layer
1814+
"""
1815+
time_info = {}
1816+
gs_layer = gs_catalog.get_layer(name=layer.name)
1817+
if gs_layer is not None:
1818+
gs_time_info = gs_layer.resource.metadata.get("time")
1819+
if gs_time_info.enabled:
1820+
_attr = layer.attributes.filter(attribute=gs_time_info.attribute).first()
1821+
time_info["attribute"] = _attr.pk if _attr else None
1822+
if gs_time_info.end_attribute is not None:
1823+
end_attr = layer.attributes.filter(attribute=gs_time_info.end_attribute).first()
1824+
time_info["end_attribute"] = end_attr.pk if end_attr else None
1825+
time_info["presentation"] = gs_time_info.presentation
1826+
lookup_value = sorted(list(gs_time_info._lookup), key=lambda x: x[1], reverse=True)
1827+
if gs_time_info.resolution is not None:
1828+
res = gs_time_info.resolution // 1000
1829+
for el in lookup_value:
1830+
if res % el[1] == 0:
1831+
time_info["precision_value"] = res // el[1]
1832+
time_info["precision_step"] = el[0]
1833+
break
1834+
else:
1835+
time_info["precision_value"] = gs_time_info.resolution
1836+
time_info["precision_step"] = "seconds"
1837+
return time_info
1838+
else:
1839+
return None
1840+
1841+
18111842
ogc_server_settings = OGC_Servers_Handler(settings.OGC_SERVER)["default"]
18121843

18131844
_wms = None

geonode/geoserver/tests/test_helpers.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,21 @@
3131
from geonode.layers.populate_datasets_data import create_dataset_data
3232
from geonode.tests.base import GeoNodeBaseTestSupport
3333
from geonode.geoserver.views import _response_callback
34+
from geonode.layers.models import Dataset, Attribute
35+
from uuid import uuid4
36+
from django.contrib.auth import get_user_model
37+
3438
from geonode.geoserver.helpers import (
3539
gs_catalog,
3640
ows_endpoint_in_path,
3741
get_dataset_storetype,
3842
extract_name_from_sld,
3943
get_dataset_capabilities_url,
4044
get_layer_ows_url,
45+
get_time_info,
4146
)
4247
from geonode.geoserver.ows import _wcs_link, _wfs_link, _wms_link
48+
from unittest.mock import patch, Mock
4349

4450

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

268274
@on_ogc_backend(geoserver.BACKEND_PACKAGE)
269275
def test_layer_ows_url(self):
270-
from geonode.layers.models import Dataset
271276

272277
ows_url = settings.GEOSERVER_PUBLIC_LOCATION
273278
identifier = "geonode:CA"
274279
dataset = Dataset.objects.get(alternate=identifier)
275280
expected_url = f"{ows_url}geonode/CA/ows"
276281
capabilities_url = get_layer_ows_url(dataset)
277282
self.assertEqual(capabilities_url, expected_url, capabilities_url)
283+
284+
# Tests for geonode.geoserver.helpers.get_time_info
285+
@patch("geonode.geoserver.helpers.gs_catalog")
286+
def test_get_time_info_valid_layer(self, mock_gs_catalog):
287+
288+
mock_dataset = Dataset.objects.create(
289+
uuid=str(uuid4()),
290+
owner=get_user_model().objects.get(username=self.user),
291+
name="geonode:states",
292+
store="httpfooremoteservce",
293+
subtype="remote",
294+
alternate="geonode:states",
295+
)
296+
297+
Attribute.objects.create(pk=5, attribute="begin", dataset_id=mock_dataset.pk)
298+
299+
Attribute.objects.create(pk=6, attribute="end", dataset_id=mock_dataset.pk)
300+
301+
# Build mock GeoServer's time info
302+
mock_gs_time_info = Mock()
303+
mock_gs_time_info.enabled = True
304+
mock_gs_time_info.attribute = "begin"
305+
mock_gs_time_info.end_attribute = "end"
306+
mock_gs_time_info.presentation = "DISCRETE_INTERVAL"
307+
mock_gs_time_info.resolution = 5000
308+
mock_gs_time_info._lookup = [("seconds", 1), ("minutes", 60)]
309+
310+
mock_gs_layer = Mock()
311+
mock_gs_layer.resource.metadata.get.return_value = mock_gs_time_info
312+
mock_gs_catalog.get_layer.return_value = mock_gs_layer
313+
314+
result = get_time_info(mock_dataset)
315+
316+
self.assertEqual(result["attribute"], 5)
317+
self.assertEqual(result["end_attribute"], 6)
318+
self.assertEqual(result["presentation"], "DISCRETE_INTERVAL")
319+
self.assertEqual(result["precision_value"], 5)
320+
self.assertEqual(result["precision_step"], "seconds")
321+
322+
@patch("geonode.geoserver.helpers.gs_catalog")
323+
def test_get_time_info_with_time_disabled(self, mock_gs_catalog):
324+
325+
mock_dataset = Dataset.objects.create(
326+
uuid=str(uuid4()),
327+
owner=get_user_model().objects.get(username=self.user),
328+
name="geonode:states",
329+
store="httpfooremoteservce",
330+
subtype="remote",
331+
alternate="geonode:states",
332+
)
333+
334+
Attribute.objects.create(pk=5, attribute="begin", dataset_id=mock_dataset.pk)
335+
336+
Attribute.objects.create(pk=6, attribute="end", dataset_id=mock_dataset.pk)
337+
338+
mock_gs_time_info = Mock()
339+
mock_gs_time_info.enabled = False
340+
mock_gs_time_info.attribute = "begin"
341+
mock_gs_time_info.end_attribute = "end"
342+
mock_gs_time_info.presentation = "DISCRETE_INTERVAL"
343+
mock_gs_time_info.resolution = 10000
344+
mock_gs_time_info._lookup = [("seconds", 1), ("minutes", 60)]
345+
346+
mock_gs_layer = Mock()
347+
mock_gs_layer.resource.metadata.get.return_value = mock_gs_time_info
348+
mock_gs_catalog.get_layer.return_value = mock_gs_layer
349+
350+
result = get_time_info(mock_dataset)
351+
self.assertEqual(result, {})
352+
353+
@patch("geonode.geoserver.helpers.gs_catalog")
354+
def test_get_time_info_no_layer(self, mock_gs_catalog):
355+
356+
mock_gs_catalog.get_layer.return_value = None
357+
358+
mock_layer = Mock()
359+
mock_layer.name = "nonexistent_layer"
360+
361+
result = get_time_info(mock_layer)
362+
self.assertIsNone(result)

geonode/layers/api/serializers.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,40 @@ class DatasetMetadataSerializer(serializers.Serializer):
213213

214214
class Meta:
215215
fields = "metadata_file"
216+
217+
218+
class DatasetTimeSeriesSerializer(serializers.Serializer):
219+
220+
def __init__(self, *args, **kwargs):
221+
222+
super().__init__(*args, **kwargs)
223+
224+
layer = self.context.get("layer")
225+
226+
if layer:
227+
# use the get_choices method of the Dataset model
228+
choices = [(None, "-----")] + layer.get_choices
229+
self.fields["attribute"].choices = choices
230+
self.fields["end_attribute"].choices = choices
231+
else:
232+
choices = [(None, "-----")]
233+
234+
has_time = serializers.BooleanField(default=False)
235+
attribute = serializers.ChoiceField(choices=[], required=False)
236+
end_attribute = serializers.ChoiceField(choices=[], required=False)
237+
presentation = serializers.ChoiceField(
238+
required=False,
239+
choices=[
240+
("LIST", "List of all the distinct time values"),
241+
("DISCRETE_INTERVAL", "Intervals defined by the resolution"),
242+
(
243+
"CONTINUOUS_INTERVAL",
244+
"Continuous Intervals for data that is frequently updated, resolution describes the frequency of updates",
245+
),
246+
],
247+
)
248+
precision_value = serializers.IntegerField(required=False)
249+
precision_step = serializers.ChoiceField(
250+
required=False,
251+
choices=[("years",) * 2, ("months",) * 2, ("days",) * 2, ("hours",) * 2, ("minutes",) * 2, ("seconds",) * 2],
252+
)

geonode/layers/api/views.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,25 @@
4040
from geonode.resource.utils import update_resource
4141
from geonode.resource.manager import resource_manager
4242
from rest_framework.exceptions import NotFound
43+
from django.shortcuts import get_object_or_404
44+
from django.http import JsonResponse
4345

4446
from geonode.storage.manager import StorageManager
4547

4648
from .serializers import (
4749
DatasetSerializer,
4850
DatasetListSerializer,
4951
DatasetMetadataSerializer,
52+
DatasetTimeSeriesSerializer,
5053
)
5154
from .permissions import DatasetPermissionsFilter
5255

56+
from geonode import geoserver
57+
from geonode.utils import check_ogc_backend
58+
59+
if check_ogc_backend(geoserver.BACKEND_PACKAGE):
60+
from geonode.geoserver.helpers import get_time_info
61+
5362
import logging
5463

5564
logger = logging.getLogger(__name__)
@@ -80,6 +89,8 @@ class DatasetViewSet(ApiPresetsInitializer, DynamicModelViewSet, AdvertisedListM
8089
def get_serializer_class(self):
8190
if self.action == "list":
8291
return DatasetListSerializer
92+
if self.action == "timeseries_info":
93+
return DatasetTimeSeriesSerializer
8394
return DatasetSerializer
8495

8596
def partial_update(self, request, *args, **kwargs):
@@ -187,3 +198,98 @@ def maps(self, request, pk=None, *args, **kwargs):
187198
dataset = self.get_object()
188199
resources = dataset.maps
189200
return Response(SimpleMapSerializer(many=True).to_representation(resources))
201+
202+
@action(
203+
detail=True,
204+
url_path="timeseries",
205+
url_name="timeseries",
206+
methods=["get", "put"],
207+
permission_classes=[IsAuthenticated],
208+
)
209+
def timeseries_info(self, request, pk, *args, **kwards):
210+
"""
211+
Endpoint for timeseries information
212+
213+
url = "http://localhost:8080/api/v2/datasets/{dataset_id}/timeseries"
214+
215+
cURL examples:
216+
GET method
217+
curl -X GET http://localhost:8000/api/v2/datasets/1/timeseries -u <username>:<password>
218+
219+
PUT method
220+
curl -X PUT http://localhost:8000/api/v2/datasets/1/timeseries -u <username>:<password>
221+
-H "Content-Type: application/json" -d '{"has_time": true, "attribute": 4, "end_attribute": 5,
222+
"presentation": "DISCRETE_INTERVAL", "precision_value": 2, "precision_step": "months"}'
223+
"""
224+
225+
layer = get_object_or_404(Dataset, id=pk)
226+
227+
if layer.supports_time is False:
228+
return JsonResponse({"message": "The time dimension is not supported for this dataset."}, status=200)
229+
230+
if request.method == "GET":
231+
232+
time_info = get_time_info(layer)
233+
serializer = DatasetTimeSeriesSerializer(data=time_info, context={"layer": layer})
234+
serializer.is_valid(raise_exception=True)
235+
serialized_time_info = serializer.data
236+
237+
if layer.has_time is True and time_info is not None:
238+
serialized_time_info["has_time"] = layer.has_time
239+
return JsonResponse(serialized_time_info, status=200)
240+
else:
241+
return JsonResponse({"message": "No time information available."}, status=404)
242+
243+
if request.method == "PUT":
244+
245+
serializer = DatasetTimeSeriesSerializer(data=request.data, context={"layer": layer})
246+
serializer.is_valid(raise_exception=True)
247+
serialized_time_info = serializer.validated_data
248+
249+
if serialized_time_info.get("has_time") is True:
250+
251+
start_attr = (
252+
layer.attributes.get(pk=serialized_time_info.get("attribute")).attribute
253+
if serialized_time_info.get("attribute")
254+
else None
255+
)
256+
end_attr = (
257+
layer.attributes.get(pk=serialized_time_info.get("end_attribute")).attribute
258+
if serialized_time_info.get("end_attribute")
259+
else None
260+
)
261+
262+
# Save the has_time value to the database
263+
layer.has_time = True
264+
layer.save()
265+
266+
resource_manager.exec(
267+
"set_time_info",
268+
None,
269+
instance=layer,
270+
time_info={
271+
"attribute": start_attr,
272+
"end_attribute": end_attr,
273+
"presentation": serialized_time_info.get("presentation", None),
274+
"precision_value": serialized_time_info.get("precision_value", None),
275+
"precision_step": serialized_time_info.get("precision_step", None),
276+
"enabled": serialized_time_info.get("has_time", False),
277+
},
278+
)
279+
280+
resource_manager.update(
281+
layer.uuid,
282+
instance=layer,
283+
notify=True,
284+
)
285+
return JsonResponse({"message": "the time information data was updated successfully"}, status=200)
286+
else:
287+
# Save the has_time value to the database
288+
layer.has_time = False
289+
layer.save()
290+
291+
return JsonResponse(
292+
{
293+
"message": "The time information was not updated since the time dimension is disabled for this layer"
294+
}
295+
)

geonode/layers/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@ def is_vector(self):
164164
def is_raster(self):
165165
return self.subtype == "raster"
166166

167+
@property
168+
def supports_time(self):
169+
valid_attributes = self.get_choices
170+
# check if the layer object if a vector and
171+
# includes valid_attributes
172+
if self.is_vector() and valid_attributes:
173+
return True
174+
return False
175+
176+
@property
177+
def get_choices(self):
178+
179+
attributes = Attribute.objects.filter(dataset_id=self.pk)
180+
return [(_a.pk, _a.attribute) for _a in attributes if _a.attribute_type in ["xsd:dateTime", "xsd:date"]]
181+
167182
@property
168183
def display_type(self):
169184
if self.subtype in ["vector", "vector_time"]:

0 commit comments

Comments
 (0)