Skip to content

Commit 54d85b0

Browse files
mwallschlaegergiohappyafabiani
authored
[Fixes: #10841] Serializing and deserializing ResourceBase and Dataset objects breaks validation check (#10843)
* issue10841 add convertion to internal types for DynamicModelSerializer * issue10841 add convertion to internal types for DynamicModelSerializer * issue10841 add convertion to internal types for DynamicModelSerializer * issue10841 add convertion to internal types for DynamicModelSerializer * issue10841 add convertion to internal types for DynamicModelSerializer * - Fix formatting --------- Co-authored-by: Giovanni Allegri <giohappy@gmail.com> Co-authored-by: Alessio Fabiani <alessio.fabiani@geosolutionsgroup.com>
1 parent d049668 commit 54d85b0

File tree

3 files changed

+123
-19
lines changed

3 files changed

+123
-19
lines changed

geonode/base/api/fields.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#########################################################################
2+
#
3+
# Copyright (C) 2016 OSGeo
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
#########################################################################
19+
20+
import json
21+
22+
from django.core.exceptions import ValidationError
23+
from dynamic_rest.fields.fields import DynamicRelationField
24+
25+
26+
class ComplexDynamicRelationField(DynamicRelationField):
27+
def to_internal_value_single(self, data, serializer):
28+
"""Overwrite of DynamicRelationField implementation to handle complex data structure initialization
29+
30+
Args:
31+
data (Optional[str, Dict]}): serialized or deserialized data from http calls (POST, GET ...)
32+
serializer (DynamicModelSerializer): Serializer for the given data
33+
34+
Raises:
35+
ValidationError: raised when requested data does not exist
36+
37+
django.db.models.QuerySet: return QuerySet object of the request or set data
38+
"""
39+
related_model = serializer.Meta.model
40+
if isinstance(data, str):
41+
data = json.loads(data)
42+
43+
if isinstance(data, dict):
44+
try:
45+
if hasattr(serializer, "many") and serializer.many is True:
46+
return [serializer.get_model().objects.get(**d) for d in data]
47+
return serializer.get_model().objects.get(**data)
48+
except related_model.DoesNotExist:
49+
raise ValidationError(
50+
"Invalid value for '%s': %s object with ID=%s not found"
51+
% (self.field_name, related_model.__name__, data)
52+
)
53+
else:
54+
return super().to_internal_value_single(data, serializer)

geonode/base/api/serializers.py

+16-18
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,18 @@
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
#
1818
#########################################################################
19-
import json
19+
import logging
2020
from slugify import slugify
2121
from urllib.parse import urljoin
22+
import json
2223

2324
from django.db.models import Q
2425
from django.conf import settings
2526
from django.contrib.auth.models import Group
2627
from django.forms.models import model_to_dict
2728
from django.contrib.auth import get_user_model
2829
from django.db.models.query import QuerySet
30+
from django.http import QueryDict
2931

3032
from rest_framework import serializers
3133
from rest_framework_gis import fields
@@ -51,13 +53,11 @@
5153
ExtraMetadata,
5254
)
5355
from geonode.groups.models import GroupCategory, GroupProfile
54-
56+
from geonode.base.api.fields import ComplexDynamicRelationField
5557
from geonode.utils import build_absolute_uri
5658
from geonode.security.utils import get_resources_with_perms
5759
from geonode.resource.models import ExecutionRequest
5860

59-
import logging
60-
6161
logger = logging.getLogger(__name__)
6262

6363

@@ -351,9 +351,6 @@ class Meta:
351351
model = ResourceBase
352352
fields = ("pk", "blob")
353353

354-
def to_internal_value(self, data):
355-
return data
356-
357354
def to_representation(self, value):
358355
data = ResourceBase.objects.filter(id=value)
359356
if data.exists() and data.count() == 1:
@@ -444,7 +441,7 @@ def __init__(self, *args, **kwargs):
444441
self.fields["bbox_polygon"] = fields.GeometryField(read_only=True, required=False)
445442
self.fields["ll_bbox_polygon"] = fields.GeometryField(read_only=True, required=False)
446443
self.fields["srid"] = serializers.CharField(required=False)
447-
self.fields["group"] = DynamicRelationField(GroupSerializer, embed=True, many=False)
444+
self.fields["group"] = ComplexDynamicRelationField(GroupSerializer, embed=True, many=False)
448445
self.fields["popular_count"] = serializers.CharField(required=False)
449446
self.fields["share_count"] = serializers.CharField(required=False)
450447
self.fields["rating"] = serializers.CharField(required=False)
@@ -463,28 +460,27 @@ def __init__(self, *args, **kwargs):
463460
self.fields["processed"] = serializers.BooleanField(read_only=True)
464461
self.fields["state"] = serializers.CharField(read_only=True)
465462
self.fields["sourcetype"] = serializers.CharField(read_only=True)
466-
467463
self.fields["embed_url"] = EmbedUrlField(required=False)
468464
self.fields["thumbnail_url"] = ThumbnailUrlField(read_only=True)
469-
self.fields["keywords"] = DynamicRelationField(SimpleHierarchicalKeywordSerializer, embed=False, many=True)
470-
self.fields["tkeywords"] = DynamicRelationField(SimpleThesaurusKeywordSerializer, embed=False, many=True)
465+
self.fields["keywords"] = ComplexDynamicRelationField(
466+
SimpleHierarchicalKeywordSerializer, embed=False, many=True
467+
)
468+
self.fields["tkeywords"] = ComplexDynamicRelationField(SimpleThesaurusKeywordSerializer, embed=False, many=True)
471469
self.fields["regions"] = DynamicRelationField(SimpleRegionSerializer, embed=True, many=True, read_only=True)
472-
self.fields["category"] = DynamicRelationField(SimpleTopicCategorySerializer, embed=True, many=False)
473-
self.fields["restriction_code_type"] = DynamicRelationField(
470+
self.fields["category"] = ComplexDynamicRelationField(SimpleTopicCategorySerializer, embed=True, many=False)
471+
self.fields["restriction_code_type"] = ComplexDynamicRelationField(
474472
RestrictionCodeTypeSerializer, embed=True, many=False
475473
)
476-
self.fields["license"] = DynamicRelationField(LicenseSerializer, embed=True, many=False)
477-
self.fields["spatial_representation_type"] = DynamicRelationField(
474+
self.fields["license"] = ComplexDynamicRelationField(LicenseSerializer, embed=True, many=False)
475+
self.fields["spatial_representation_type"] = ComplexDynamicRelationField(
478476
SpatialRepresentationTypeSerializer, embed=True, many=False
479477
)
480478
self.fields["blob"] = serializers.JSONField(required=False, write_only=True)
481479
self.fields["is_copyable"] = serializers.BooleanField(read_only=True)
482-
483480
self.fields["download_url"] = DownloadLinkField(read_only=True)
484-
485481
self.fields["favorite"] = FavoriteField(read_only=True)
486482

487-
metadata = DynamicRelationField(ExtraMetadataSerializer, embed=False, many=True, deferred=True)
483+
metadata = ComplexDynamicRelationField(ExtraMetadataSerializer, embed=False, many=True, deferred=True)
488484

489485
class Meta:
490486
model = ResourceBase
@@ -596,6 +592,8 @@ def to_internal_value(self, data):
596592
data = json.loads(data)
597593
if "data" in data:
598594
data["blob"] = data.pop("data")
595+
if isinstance(data, QueryDict):
596+
data = data.dict()
599597
data = super(ResourceBaseSerializer, self).to_internal_value(data)
600598
return data
601599

geonode/base/api/tests.py

+53-1
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,23 @@
3232
from uuid import uuid4
3333
from unittest.mock import patch
3434
from urllib.parse import urljoin
35+
from datetime import date, timedelta
3536

3637
from django.urls import reverse
3738
from django.conf import settings
3839
from django.core.files.uploadedfile import SimpleUploadedFile
39-
from django.contrib.auth.models import Group
4040
from django.contrib.auth import get_user_model
4141

4242
from rest_framework.test import APITestCase
43+
from rest_framework.renderers import JSONRenderer
44+
from rest_framework.parsers import JSONParser
4345

4446
from guardian.shortcuts import get_anonymous_user
4547
from geonode.maps.models import Map, MapLayer
4648
from geonode.tests.base import GeoNodeBaseTestSupport
4749

4850
from geonode.base import enumerations
51+
from geonode.base.api.serializers import ResourceBaseSerializer
4952
from geonode.groups.models import GroupMember, GroupProfile
5053
from geonode.thumbs.exceptions import ThumbnailError
5154
from geonode.layers.utils import get_files
@@ -56,6 +59,9 @@
5659
TopicCategory,
5760
ThesaurusKeyword,
5861
ExtraMetadata,
62+
RestrictionCodeType,
63+
License,
64+
Group,
5965
)
6066

6167
from geonode.layers.models import Dataset
@@ -707,6 +713,52 @@ def test_write_resources(self):
707713
self.assertEqual("321-12345-987654321", response.data["resource"]["doi"], response.data["resource"]["doi"])
708714
self.assertEqual(True, response.data["resource"]["is_published"], response.data["resource"]["is_published"])
709715

716+
def test_resource_serializer_validation(self):
717+
"""
718+
Testing serializing and deserializing of a Dataset base on django-rest description:
719+
https://www.django-rest-framework.org/api-guide/serializers/#deserializing-objects
720+
"""
721+
owner, _ = get_user_model().objects.get_or_create(username="delet-owner")
722+
title = "TEST DS TITLE"
723+
HierarchicalKeyword.add_root(name="a")
724+
keyword = HierarchicalKeyword.objects.get(slug="a")
725+
keyword.add_child(name="a1")
726+
727+
Dataset(
728+
title=title,
729+
abstract="abstract",
730+
name="test dataset",
731+
alternate="Test Remove User",
732+
attribution="Test Attribution",
733+
uuid=str(uuid4()),
734+
doi="test DOI",
735+
edition=1,
736+
maintenance_frequency=enumerations.UPDATE_FREQUENCIES[0],
737+
constraints_other="Test Constrains other",
738+
temporal_extent_start=date.today() - timedelta(days=1),
739+
temporal_extent_end=date.today(),
740+
data_quality_statement="Test data quality statement",
741+
purpose="Test Purpose",
742+
owner=owner,
743+
subtype="raster",
744+
category=TopicCategory.objects.get(identifier="elevation"),
745+
resource_type="dataset",
746+
license=License.objects.all().first(),
747+
restriction_code_type=RestrictionCodeType.objects.all().first(),
748+
group=Group.objects.all().first(),
749+
).save()
750+
751+
ds = ResourceBase.objects.get(title=title)
752+
ds.keywords.add(HierarchicalKeyword.objects.get(slug="a1"))
753+
754+
serialized = ResourceBaseSerializer(ds)
755+
json = JSONRenderer().render(serialized.data)
756+
stream = BytesIO(json)
757+
data = JSONParser().parse(stream)
758+
self.assertIsInstance(data, dict)
759+
se = ResourceBaseSerializer(data=data)
760+
self.assertTrue(se.is_valid())
761+
710762
def test_delete_user_with_resource(self):
711763
owner, created = get_user_model().objects.get_or_create(username="delet-owner")
712764
Dataset(

0 commit comments

Comments
 (0)