Skip to content

Commit 153736b

Browse files
ahmdthrafabiani
andauthored
[Fixes GeoNode#10482] Upload ISO-19115 xml metadata via the API (GeoNode#10483)
* ISO-19115 xml metadata file can be uploaded via the API * Small changes for PEP8 compliance * Added tests for checking permissions for metadata upload Co-authored-by: Alessio Fabiani <[email protected]>
1 parent 14f8102 commit 153736b

File tree

4 files changed

+215
-3
lines changed

4 files changed

+215
-3
lines changed

geonode/layers/api/exceptions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,17 @@ class InvalidDatasetException(APIException):
3131
default_detail = "Input payload is not valid"
3232
default_code = "invalid_dataset_exception"
3333
category = "dataset_api"
34+
35+
36+
class InvalidMetadataException(APIException):
37+
status_code = 500
38+
default_detail = "Input payload is not valid"
39+
default_code = "invalid_metadata_exception"
40+
category = "dataset_api"
41+
42+
43+
class MissingMetadataException(APIException):
44+
status_code = 400
45+
default_detail = "Metadata is missing"
46+
default_code = "missing_metadata_exception"
47+
category = "dataset_api"

geonode/layers/api/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,18 @@ class Meta:
179179
xml_file = serializers.CharField(required=False)
180180
sld_file = serializers.CharField(required=False)
181181
store_spatial_files = serializers.BooleanField(required=False, default=True)
182+
183+
184+
class MetadataFileField(DynamicComputedField):
185+
186+
def get_attribute(self, instance):
187+
return instance.get('metadata_file')
188+
189+
190+
class DatasetMetadataSerializer(serializers.Serializer):
191+
metadata_file = MetadataFileField(required=True)
192+
193+
class Meta:
194+
fields = (
195+
"metadata_file"
196+
)

geonode/layers/api/tests.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
#
1818
#########################################################################
19+
from io import BytesIO
1920
import logging
2021

2122
from unittest.mock import patch
@@ -28,6 +29,7 @@
2829
from geonode.geoserver.createlayer.utils import create_dataset
2930
from guardian.shortcuts import assign_perm, get_anonymous_user
3031

32+
from django.conf import settings
3133
from geonode.layers.models import Dataset, Attribute
3234
from geonode.base.populate_test_data import create_models, create_single_dataset
3335
from geonode.maps.models import Map, MapLayer
@@ -44,6 +46,7 @@ class DatasetsApiTests(APITestCase):
4446
]
4547

4648
def setUp(self):
49+
self.exml_path = f"{settings.PROJECT_ROOT}/base/fixtures/test_xml.xml"
4750
create_models(b'document')
4851
create_models(b'map')
4952
create_models(b'dataset')
@@ -312,3 +315,96 @@ def test_layer_replace_should_work(self, _validate_input_source):
312315
layer.refresh_from_db()
313316
# evaluate that the number of available layer is not changed
314317
self.assertEqual(Dataset.objects.count(), cnt)
318+
319+
def test_metadata_update_for_not_supported_method(self):
320+
layer = Dataset.objects.first()
321+
url = reverse("datasets-replace-metadata", args=(layer.id,))
322+
self.client.login(username="admin", password="admin")
323+
324+
response = self.client.post(url)
325+
self.assertEqual(405, response.status_code)
326+
327+
response = self.client.get(url)
328+
self.assertEqual(405, response.status_code)
329+
330+
def test_metadata_update_for_not_authorized_user(self):
331+
layer = Dataset.objects.first()
332+
url = reverse("datasets-replace-metadata", args=(layer.id,))
333+
334+
response = self.client.put(url)
335+
self.assertEqual(403, response.status_code)
336+
337+
def test_unsupported_file_throws_error(self):
338+
layer = Dataset.objects.first()
339+
url = reverse("datasets-replace-metadata", args=(layer.id,))
340+
self.client.login(username="admin", password="admin")
341+
342+
data = '<?xml version="1.0" encoding="UTF-8"?><invalid></invalid>'
343+
f = BytesIO(bytes(data, encoding='utf-8'))
344+
f.name = 'metadata.xml'
345+
put_data = {'metadata_file': f}
346+
response = self.client.put(url, data=put_data)
347+
self.assertEqual(500, response.status_code)
348+
349+
def test_valid_metadata_file_with_different_uuid(self):
350+
layer = Dataset.objects.first()
351+
url = reverse("datasets-replace-metadata", args=(layer.id,))
352+
self.client.login(username="admin", password="admin")
353+
354+
f = open(self.exml_path, 'r')
355+
put_data = {'metadata_file': f}
356+
response = self.client.put(url, data=put_data)
357+
self.assertEqual(500, response.status_code)
358+
359+
def test_permissions_for_not_permitted_user(self):
360+
get_user_model().objects.create_user(
361+
username="some_user",
362+
password="some_password",
363+
364+
)
365+
layer = Dataset.objects.first()
366+
url = reverse("datasets-replace-metadata", args=(layer.id,))
367+
self.client.login(username="some_user", password="some_password")
368+
369+
uuid = layer.uuid
370+
data = open(self.exml_path).read()
371+
data = data.replace('7cfbc42c-efa7-431c-8daa-1399dff4cd19', uuid)
372+
f = BytesIO(bytes(data, encoding='utf-8'))
373+
f.name = 'metadata.xml'
374+
put_data = {'metadata_file': f}
375+
response = self.client.put(url, data=put_data)
376+
self.assertEqual(403, response.status_code)
377+
378+
def test_permissions_for_permitted_user(self):
379+
another_non_admin_user = get_user_model().objects.create_user(
380+
username="some_other_user",
381+
password="some_other_password",
382+
383+
)
384+
layer = Dataset.objects.first()
385+
assign_perm("base.change_resourcebase_metadata", another_non_admin_user, layer.get_self_resource())
386+
url = reverse("datasets-replace-metadata", args=(layer.id,))
387+
self.client.login(username="some_other_user", password="some_other_password")
388+
389+
uuid = layer.uuid
390+
data = open(self.exml_path).read()
391+
data = data.replace('7cfbc42c-efa7-431c-8daa-1399dff4cd19', uuid)
392+
f = BytesIO(bytes(data, encoding='utf-8'))
393+
f.name = 'metadata.xml'
394+
put_data = {'metadata_file': f}
395+
response = self.client.put(url, data=put_data)
396+
self.assertEqual(200, response.status_code)
397+
398+
def test_valid_metadata_file(self):
399+
layer = Dataset.objects.first()
400+
url = reverse("datasets-replace-metadata", args=(layer.id,))
401+
self.client.login(username="admin", password="admin")
402+
403+
uuid = layer.uuid
404+
data = open(self.exml_path).read()
405+
data = data.replace('7cfbc42c-efa7-431c-8daa-1399dff4cd19', uuid)
406+
f = BytesIO(bytes(data, encoding='utf-8'))
407+
f.name = 'metadata.xml'
408+
put_data = {'metadata_file': f}
409+
response = self.client.put(url, data=put_data)
410+
self.assertEqual(200, response.status_code)

geonode/layers/api/views.py

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from dynamic_rest.filters import DynamicFilterBackend, DynamicSortingFilter
2424

2525
from rest_framework.decorators import action
26-
from rest_framework.permissions import IsAuthenticatedOrReadOnly
26+
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated
2727
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
2828

2929
from oauth2_provider.contrib.rest_framework import OAuth2Authentication
@@ -32,16 +32,25 @@
3232
from geonode.base.api.filters import DynamicSearchFilter, ExtentFilter
3333
from geonode.base.api.pagination import GeoNodeApiPagination
3434
from geonode.base.api.permissions import UserHasPerms
35-
from geonode.layers.api.exceptions import GeneralDatasetException, InvalidDatasetException
35+
from geonode.layers.api.exceptions import (
36+
GeneralDatasetException,
37+
InvalidDatasetException,
38+
InvalidMetadataException)
39+
from geonode.layers.metadata import parse_metadata
3640
from geonode.layers.models import Dataset
3741
from geonode.layers.utils import validate_input_source
3842
from geonode.maps.api.serializers import SimpleMapLayerSerializer, SimpleMapSerializer
43+
from geonode.resource.utils import update_resource
3944
from rest_framework.exceptions import NotFound
4045

4146
from geonode.storage.manager import StorageManager
4247
from geonode.resource.manager import resource_manager
4348

44-
from .serializers import DatasetReplaceAppendSerializer, DatasetSerializer, DatasetListSerializer
49+
from .serializers import (
50+
DatasetReplaceAppendSerializer,
51+
DatasetSerializer,
52+
DatasetListSerializer,
53+
DatasetMetadataSerializer)
4554
from .permissions import DatasetPermissionsFilter
4655

4756
import logging
@@ -72,6 +81,84 @@ def get_serializer_class(self):
7281
return DatasetListSerializer
7382
return DatasetSerializer
7483

84+
@extend_schema(
85+
request=DatasetMetadataSerializer,
86+
methods=["put"],
87+
responses={200},
88+
description="API endpoint to upload metadata file.",
89+
)
90+
@action(
91+
detail=False,
92+
url_path="(?P<pk>\d+)/metadata", # noqa
93+
url_name="replace-metadata",
94+
methods=["put"],
95+
serializer_class=DatasetMetadataSerializer,
96+
permission_classes=[IsAuthenticated, UserHasPerms(
97+
perms_dict={
98+
"default": {
99+
"PUT": ['base.change_resourcebase_metadata']
100+
}
101+
}
102+
)]
103+
)
104+
def metadata(self, request, pk=None):
105+
"""
106+
Endpoint to upload ISO metadata
107+
Usage Example:
108+
109+
import requests
110+
111+
dataset_id = 1
112+
url = f"http://localhost:8080/api/v2/datasets/{dataset_id}/metadata"
113+
files=[
114+
('metadata_file',('metadata.xml',open('/home/user/metadata.xml','rb'),'text/xml'))
115+
]
116+
headers = {
117+
'Authorization': 'Basic dXNlcjpwYXNzd29yZA=='
118+
}
119+
response = requests.request("PUT", url, payload={}, files=files)
120+
121+
cURL example:
122+
curl --location --request PUT 'http://localhost:8000/api/v2/datasets/{dataset_id}/metadata' \
123+
--form 'metadata_file=@/home/user/metadata.xml'
124+
"""
125+
out = {}
126+
storage_manager = None
127+
if not self.queryset.filter(id=pk).exists():
128+
raise NotFound(detail=f"Dataset with ID {pk} is not available")
129+
serializer = self.serializer_class(data=request.data)
130+
if not serializer.is_valid(raise_exception=False):
131+
raise InvalidDatasetException(detail=serializer.errors)
132+
try:
133+
data = serializer.data.copy()
134+
if not data["metadata_file"]:
135+
raise InvalidMetadataException(detail="A valid metadata file must be specified")
136+
storage_manager = StorageManager(remote_files=data)
137+
storage_manager.clone_remote_files()
138+
file = storage_manager.get_retrieved_paths()
139+
metadata_file = file["metadata_file"]
140+
dataset = self.queryset.get(id=pk)
141+
try:
142+
dataset_uuid, vals, regions, keywords, _ = parse_metadata(
143+
open(metadata_file).read())
144+
except Exception:
145+
raise InvalidMetadataException(detail="Unsupported metadata format")
146+
if dataset_uuid and dataset.uuid != dataset_uuid:
147+
raise InvalidMetadataException(detail="The UUID identifier from the XML Metadata, is different from the one saved")
148+
try:
149+
updated_dataset = update_resource(dataset, metadata_file, regions, keywords, vals)
150+
updated_dataset.save() # This also triggers the recreation of the XML metadata file according to the updated values
151+
except Exception:
152+
raise GeneralDatasetException(detail="Failed to update metadata")
153+
out['success'] = True
154+
out['message'] = ['Metadata successfully updated']
155+
return Response(out)
156+
except Exception as e:
157+
raise e
158+
finally:
159+
if storage_manager:
160+
storage_manager.delete_retrieved_paths()
161+
75162
@extend_schema(
76163
methods=["get"],
77164
responses={200: SimpleMapLayerSerializer(many=True)},

0 commit comments

Comments
 (0)